瀏覽代碼

Merge remote-tracking branch 'yudao/feature/iot' into iot

puhui999 8 月之前
父節點
當前提交
c5894765b5
共有 58 個文件被更改,包括 2504 次插入236 次删除
  1. 8 0
      yudao-dependencies/pom.xml
  2. 58 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java
  3. 1 0
      yudao-module-iot/pom.xml
  4. 3 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java
  5. 19 4
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
  6. 53 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java
  7. 58 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java
  8. 52 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java
  9. 10 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java
  10. 6 0
      yudao-module-iot/yudao-module-iot-biz/pom.xml
  11. 78 5
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
  12. 88 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java
  13. 37 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java
  14. 23 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java
  15. 2 50
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java
  16. 23 6
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
  17. 17 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java
  18. 21 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java
  19. 27 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java
  20. 30 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java
  21. 26 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java
  22. 96 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/PluginInfoController.java
  23. 59 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoPageReqVO.java
  24. 71 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoRespVO.java
  25. 49 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoSaveReqVO.java
  26. 94 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/PluginInstanceController.java
  27. 37 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstancePageReqVO.java
  28. 43 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstanceRespVO.java
  29. 35 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstanceSaveReqVO.java
  30. 3 3
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java
  31. 2 2
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
  32. 1 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
  33. 22 8
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
  34. 44 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java
  35. 83 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugininfo/PluginInfoDO.java
  36. 51 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugininstance/PluginInstanceDO.java
  37. 35 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java
  38. 23 14
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
  39. 36 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugininfo/PluginInfoMapper.java
  40. 29 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugininstance/PluginInstanceMapper.java
  41. 1 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java
  42. 1 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java
  43. 15 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/SpringConfiguration.java
  44. 0 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceDataService.java
  45. 96 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java
  46. 94 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java
  47. 54 7
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
  48. 234 127
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
  49. 72 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoService.java
  50. 267 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoServiceImpl.java
  51. 54 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininstance/PluginInstanceService.java
  52. 70 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininstance/PluginInstanceServiceImpl.java
  53. 16 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java
  54. 16 6
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
  55. 12 0
      yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/plugininfo/PluginInfoMapper.xml
  56. 12 0
      yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/plugininstance/PluginInstanceMapper.xml
  57. 31 0
      yudao-module-iot/yudao-module-iot-plugin/pom.xml
  58. 6 0
      yudao-module-iot/yudao-module-iot-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/package-info.java

+ 8 - 0
yudao-dependencies/pom.xml

@@ -66,6 +66,7 @@
         <ip2region.version>2.7.0</ip2region.version>
         <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
         <mqtt.version>1.2.5</mqtt.version>
+        <pf4j-spring.version>0.9.0</pf4j-spring.version>
         <!-- 三方云服务相关 -->
         <okio.version>3.5.0</okio.version>
         <okhttp3.version>4.11.0</okhttp3.version>
@@ -605,6 +606,13 @@
                 <version>${mqtt.version}</version>
             </dependency>
 
+            <!-- PF4J -->
+            <dependency>
+                <groupId>org.pf4j</groupId>
+                <artifactId>pf4j-spring</artifactId>
+                <version>${pf4j-spring.version}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.mybatis.core.type;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+import org.apache.ibatis.type.TypeHandler;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Set<Long> 的类型转换器实现类,对应数据库的 varchar 类型
+ *
+ * @author 芋道源码
+ */
+@MappedJdbcTypes(JdbcType.VARCHAR)
+@MappedTypes(List.class)
+public class LongSetTypeHandler implements TypeHandler<Set<Long>> {
+
+    private static final String COMMA = ",";
+
+    @Override
+    public void setParameter(PreparedStatement ps, int i, Set<Long> strings, JdbcType jdbcType) throws SQLException {
+        // 设置占位符
+        ps.setString(i, CollUtil.join(strings, COMMA));
+    }
+
+    @Override
+    public Set<Long> getResult(ResultSet rs, String columnName) throws SQLException {
+        String value = rs.getString(columnName);
+        return getResult(value);
+    }
+
+    @Override
+    public Set<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
+        String value = rs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    @Override
+    public Set<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
+        String value = cs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    private Set<Long> getResult(String value) {
+        if (value == null) {
+            return null;
+        }
+        return StrUtils.splitToLongSet(value, COMMA);
+    }
+}

+ 1 - 0
yudao-module-iot/pom.xml

@@ -10,6 +10,7 @@
     <modules>
         <module>yudao-module-iot-api</module>
         <module>yudao-module-iot-biz</module>
+        <module>yudao-module-iot-plugin</module>
     </modules>
     <modelVersion>4.0.0</modelVersion>
 

+ 3 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java

@@ -13,4 +13,7 @@ public class DictTypeConstants {
     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_STATUS = "iot_device_status";
+
 }

+ 19 - 4
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java

@@ -26,11 +26,26 @@ public interface ErrorCodeConstants {
     ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
     ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
     ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除");
-    ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改");
-    ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改");
-    ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态");
+    ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
+    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, "导入设备数据不能为空!");
 
     // ========== 产品分类 1-050-004-000 ==========
     ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
 
-}
+    // ========== 设备分组 1-050-005-000 ==========
+    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_INFO_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_INFO_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件");
+    ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效");
+
+    // ========== 插件实例 1-050-007-000 ==========
+    ErrorCode PLUGIN_INSTANCE_NOT_EXISTS = new ErrorCode(1_050_007_000, "插件实例不存在");
+
+}

+ 53 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.iot.enums.plugin;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * IoT 部署方式枚举
+ *
+ * @author haohao
+ */
+@Getter
+public enum IotPluginDeployTypeEnum implements IntArrayValuable {
+
+    UPLOAD(0, "上传 jar"), // TODO @haohao:UPLOAD 和 ALONE 感觉有点冲突,前者是部署方式,后者是运行方式。这个后续再讨论下哈
+    ALONE(1, "独立运行");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginDeployTypeEnum::getDeployType).toArray();
+
+    /**
+     * 部署方式
+     */
+    private final Integer deployType;
+
+    /**
+     * 部署方式名
+     */
+    private final String name;
+
+    IotPluginDeployTypeEnum(Integer deployType, String name) {
+        this.deployType = deployType;
+        this.name = name;
+    }
+
+    public static IotPluginDeployTypeEnum fromDeployType(Integer deployType) {
+        for (IotPluginDeployTypeEnum value : values()) {
+            if (value.getDeployType().equals(deployType)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public static boolean isValidDeployType(Integer deployType) {
+        return fromDeployType(deployType) != null;
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 58 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.iot.enums.plugin;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * IoT 插件状态枚举
+ *
+ * @author haohao
+ */
+@Getter
+public enum IotPluginStatusEnum implements IntArrayValuable {
+
+    STOPPED(0, "停止"),
+    RUNNING(1, "运行");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态
+     */
+    private final Integer status;
+
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    IotPluginStatusEnum(Integer status, String name) {
+        this.status = status;
+        this.name = name;
+    }
+
+    public static IotPluginStatusEnum fromState(Integer state) {
+        for (IotPluginStatusEnum value : values()) {
+            if (value.getStatus().equals(state)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isValidState(Integer state) {
+        return fromState(state) != null;
+    }
+
+    public static boolean contains(Integer status) {
+        return Arrays.stream(values()).anyMatch(e -> e.getStatus().equals(status));
+    }
+
+}

+ 52 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.iot.enums.plugin;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * IoT 插件类型枚举
+ *
+ * @author haohao
+ */
+@AllArgsConstructor
+@Getter
+public enum IotPluginTypeEnum implements IntArrayValuable {
+
+    NORMAL(0, "普通插件"),
+    DEVICE(1, "设备插件");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    // TODO @haohao:可以使用 hutool 简化
+    public static IotPluginTypeEnum fromType(Integer type) {
+        for (IotPluginTypeEnum value : values()) {
+            if (value.getType().equals(type)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public static boolean isValidType(Integer type) {
+        return fromType(type) != null;
+    }
+
+}

+ 10 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java

@@ -36,4 +36,14 @@ public enum IotProductDeviceTypeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    /**
+     * 判断是否是网关
+     *
+     * @param type 类型
+     * @return 是否是网关
+     */
+    public static boolean isGateway(Integer type) {
+        return GATEWAY.getType().equals(type);
+    }
+
 }

+ 6 - 0
yudao-module-iot/yudao-module-iot-biz/pom.xml

@@ -69,6 +69,12 @@
             <groupId>org.eclipse.paho</groupId>
             <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
         </dependency>
+
+        <!-- PF4J -->
+        <dependency>
+            <groupId>org.pf4j</groupId>
+            <artifactId>pf4j-spring</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 78 - 5
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java

@@ -1,24 +1,33 @@
 package cn.iocoder.yudao.module.iot.controller.admin.device;
 
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceRespVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+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.service.device.IotDeviceService;
 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.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
 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
@@ -52,8 +61,16 @@ public class IotDeviceController {
         return success(true);
     }
 
+    @PutMapping("/update-group")
+    @Operation(summary = "更新设备分组")
+    @PreAuthorize("@ss.hasPermission('iot:device:update')")
+    public CommonResult<Boolean> updateDeviceGroup(@Valid @RequestBody IotDeviceUpdateGroupReqVO updateReqVO) {
+        deviceService.updateDeviceGroup(updateReqVO);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
-    @Operation(summary = "删除设备")
+    @Operation(summary = "删除单个设备")
     @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('iot:device:delete')")
     public CommonResult<Boolean> deleteDevice(@RequestParam("id") Long id) {
@@ -61,6 +78,15 @@ public class IotDeviceController {
         return success(true);
     }
 
+    @DeleteMapping("/delete-list")
+    @Operation(summary = "删除多个设备")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('iot:device:delete')")
+    public CommonResult<Boolean> deleteDeviceList(@RequestParam("ids") Collection<Long> ids) {
+        deviceService.deleteDeviceList(ids);
+        return success(true);
+    }
+
     @GetMapping("/get")
     @Operation(summary = "获得设备")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
@@ -78,6 +104,19 @@ public class IotDeviceController {
         return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class));
     }
 
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出设备 Excel")
+    @PreAuthorize("@ss.hasPermission('iot:device:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportDeviceExcel(@Valid IotDevicePageReqVO exportReqVO,
+            HttpServletResponse response) throws IOException {
+        exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        CommonResult<PageResult<IotDeviceRespVO>> result = getDevicePage(exportReqVO);
+        // 导出 Excel
+        ExcelUtils.write(response, "设备.xls", "数据", IotDeviceRespVO.class,
+                result.getData().getList());
+    }
+
     @GetMapping("/count")
     @Operation(summary = "获得设备数量")
     @Parameter(name = "productId", description = "产品编号", example = "1")
@@ -86,4 +125,38 @@ public class IotDeviceController {
         return success(deviceService.getDeviceCountByProductId(productId));
     }
 
+    @GetMapping("/simple-list")
+    @Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项")
+    @Parameter(name = "deviceType", description = "设备类型", example = "1")
+    public CommonResult<List<IotDeviceRespVO>> getSimpleDeviceList(
+            @RequestParam(value = "deviceType", required = false) Integer deviceType) {
+        List<IotDeviceDO> list = deviceService.getDeviceList(deviceType);
+        return success(convertList(list, device -> // 只返回 id、name 字段
+        new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName())));
+    }
+
+    @PostMapping("/import")
+    @Operation(summary = "导入设备")
+    @PreAuthorize("@ss.hasPermission('iot:device:import')")
+    public CommonResult<IotDeviceImportRespVO> importDevice(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport)
+            throws Exception {
+        List<IotDeviceImportExcelVO> list = ExcelUtils.read(file, IotDeviceImportExcelVO.class);
+        return success(deviceService.importDevice(list, updateSupport));
+    }
+
+    @GetMapping("/get-import-template")
+    @Operation(summary = "获得导入设备模板")
+    public void importTemplate(HttpServletResponse response) throws IOException {
+        // 手动创建导出 demo
+        List<IotDeviceImportExcelVO> list = Arrays.asList(
+                IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110")
+                        .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(),
+                IotDeviceImportExcelVO.builder().deviceName("biubiu")
+                        .productKey("YzvHxd4r67sT4s2B").groupNames("").build());
+        // 输出
+        ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list);
+    }
+
 }

+ 88 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java

@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device;
+
+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.device.vo.group.IotDeviceGroupPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupRespVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
+import cn.iocoder.yudao.module.iot.service.device.IotDeviceGroupService;
+import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
+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/device-group")
+@Validated
+public class IotDeviceGroupController {
+
+    @Resource
+    private IotDeviceGroupService deviceGroupService;
+    @Resource
+    private IotDeviceService deviceService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建设备分组")
+    @PreAuthorize("@ss.hasPermission('iot:device-group:create')")
+    public CommonResult<Long> createDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO createReqVO) {
+        return success(deviceGroupService.createDeviceGroup(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新设备分组")
+    @PreAuthorize("@ss.hasPermission('iot:device-group:update')")
+    public CommonResult<Boolean> updateDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO updateReqVO) {
+        deviceGroupService.updateDeviceGroup(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除设备分组")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('iot:device-group:delete')")
+    public CommonResult<Boolean> deleteDeviceGroup(@RequestParam("id") Long id) {
+        deviceGroupService.deleteDeviceGroup(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得设备分组")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('iot:device-group:query')")
+    public CommonResult<IotDeviceGroupRespVO> getDeviceGroup(@RequestParam("id") Long id) {
+        IotDeviceGroupDO deviceGroup = deviceGroupService.getDeviceGroup(id);
+        return success(BeanUtils.toBean(deviceGroup, IotDeviceGroupRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得设备分组分页")
+    @PreAuthorize("@ss.hasPermission('iot:device-group:query')")
+    public CommonResult<PageResult<IotDeviceGroupRespVO>> getDeviceGroupPage(@Valid IotDeviceGroupPageReqVO pageReqVO) {
+        PageResult<IotDeviceGroupDO> pageResult = deviceGroupService.getDeviceGroupPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, IotDeviceGroupRespVO.class,
+                group -> group.setDeviceCount(deviceService.getDeviceCountByGroupId(group.getId()))));
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获取设备分组的精简信息列表", description = "只包含被开启的分组,主要用于前端的下拉选项")
+    public CommonResult<List<IotDeviceGroupRespVO>> getSimpleDeviceGroupList() {
+        List<IotDeviceGroupDO> list = deviceGroupService.getDeviceGroupListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, group -> // 只返回 id、name 字段
+                new IotDeviceGroupRespVO().setId(group.getId()).setName(group.getName())));
+    }
+
+}

+ 37 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+/**
+ * 设备 Excel 导入 VO
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = false) // 设置 chain = false,避免设备导入有问题
+public class IotDeviceImportExcelVO {
+
+    @ExcelProperty("设备名称")
+    @NotEmpty(message = "设备名称不能为空")
+    private String deviceName;
+
+    @ExcelProperty("父设备名称")
+    @Schema(description = "父设备名称", example = "网关001")
+    private String parentDeviceName;
+
+    @ExcelProperty("产品标识")
+    @NotEmpty(message = "产品标识不能为空")
+    private String productKey;
+
+    @ExcelProperty("设备分组")
+    private String groupNames;
+
+}

+ 23 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.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 lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "管理后台 - IoT 设备导入 Response VO")
+@Data
+@Builder
+public class IotDeviceImportRespVO {
+
+    @Schema(description = "创建成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> createDeviceNames;
+
+    @Schema(description = "更新成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> updateDeviceNames;
+
+    @Schema(description = "导入失败的设备集合,key为设备名称,value为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Map<String, String> failureDeviceNames;
+} 

+ 2 - 50
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java

@@ -8,11 +8,6 @@ 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
@@ -20,11 +15,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class IotDevicePageReqVO extends PageParam {
 
-    // TODO @芋艿:需要去掉一些多余的字段;
-
-    @Schema(description = "设备唯一标识符", example = "24602")
-    private String deviceKey;
-
     @Schema(description = "设备名称", example = "王五")
     private String deviceName;
 
@@ -34,53 +24,15 @@ public class IotDevicePageReqVO extends PageParam {
     @Schema(description = "产品编号", example = "26202")
     private Long productId;
 
-    @Schema(description = "产品标识")
-    private String productKey;
-
     @Schema(description = "设备类型", example = "1")
     @InEnum(IotProductDeviceTypeEnum.class)
     private Integer deviceType;
 
-    @Schema(description = "网关设备 ID", example = "16380")
-    private Long gatewayId;
-
     @Schema(description = "设备状态", example = "1")
     @InEnum(IotDeviceStatusEnum.class)
     private Integer status;
 
-    @Schema(description = "设备状态最后更新时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] statusLastUpdateTime;
-
-    @Schema(description = "最后上线时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] lastOnlineTime;
-
-    @Schema(description = "最后离线时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] lastOfflineTime;
-
-    @Schema(description = "设备激活时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] activeTime;
-
-    @Schema(description = "设备密钥,用于设备认证,需安全存储")
-    private String deviceSecret;
-
-    @Schema(description = "MQTT 客户端 ID", example = "24602")
-    private String mqttClientId;
-
-    @Schema(description = "MQTT 用户名", example = "芋艿")
-    private String mqttUsername;
-
-    @Schema(description = "MQTT 密码")
-    private String mqttPassword;
-
-    @Schema(description = "认证类型(如一机一密、动态注册)", example = "2")
-    private String authType;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
+    @Schema(description = "设备分组编号", example = "1024")
+    private Long groupId;
 
 }

+ 23 - 6
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java

@@ -1,11 +1,16 @@
 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 com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.Set;
+
+import static cn.iocoder.yudao.module.iot.enums.DictTypeConstants.DEVICE_STATUS;
 
 @Schema(description = "管理后台 - IoT 设备 Response VO")
 @Data
@@ -20,9 +25,24 @@ public class IotDeviceRespVO {
     private String deviceKey;
 
     @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
-    @ExcelProperty("设备名称")
+    @ExcelProperty("设备名称")
     private String deviceName;
 
+    @Schema(description = "设备备注名称", example = "张三")
+    @ExcelProperty("设备备注名称")
+    private String nickname;
+
+    @Schema(description = "设备序列号", example = "1024")
+    @ExcelProperty("设备序列号")
+    private String serialNumber;
+
+    @Schema(description = "设备图片", example = "我是一名码农")
+    @ExcelProperty("设备图片")
+    private String picUrl;
+
+    @Schema(description = "设备分组编号数组", example = "1,2")
+    private Set<Long> groupIds;
+
     @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202")
     @ExcelProperty("产品编号")
     private Long productId;
@@ -35,15 +55,12 @@ public class IotDeviceRespVO {
     @ExcelProperty("设备类型")
     private Integer deviceType;
 
-    @Schema(description = "设备备注名称", example = "张三")
-    @ExcelProperty("设备备注名称")
-    private String nickname;
-
     @Schema(description = "网关设备 ID", example = "16380")
     private Long gatewayId;
 
     @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @ExcelProperty("设备状态")
+    @ExcelProperty(value = "设备状态", converter = DictConvert.class)
+    @DictFormat(DEVICE_STATUS)
     private Integer status;
 
     @Schema(description = "设备状态最后更新时间")

+ 17 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.util.Set;
+
 @Schema(description = "管理后台 - IoT 设备新增/修改 Request VO")
 @Data
 public class IotDeviceSaveReqVO {
@@ -10,13 +12,28 @@ public class IotDeviceSaveReqVO {
     @Schema(description = "设备编号", example = "177")
     private Long id;
 
+    @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177")
+    private String deviceKey;
+
     @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
     private String deviceName;
 
     @Schema(description = "备注名称", example = "张三")
     private String nickname;
 
+    @Schema(description = "设备序列号", example = "123456")
+    private String serialNumber;
+
+    @Schema(description = "设备图片", example = "https://iocoder.cn/1.png")
+    private String picUrl;
+
+    @Schema(description = "设备分组编号数组", example = "1,2")
+    private Set<Long> groupIds;
+
     @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202")
     private Long productId;
 
+    @Schema(description = "网关设备 ID", example = "16380")
+    private Long gatewayId;
+
 }

+ 21 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.Set;
+
+@Schema(description = "管理后台 - IoT 设备更新分组 Request VO")
+@Data
+public class IotDeviceUpdateGroupReqVO {
+
+    @Schema(description = "设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
+    @NotEmpty(message = "设备编号列表不能为空")
+    private Set<Long> ids;
+
+    @Schema(description = "分组编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
+    @NotEmpty(message = "分组编号列表不能为空")
+    private Set<Long> groupIds;
+
+}

+ 27 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java

@@ -0,0 +1,27 @@
+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;
+
+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 IotDeviceGroupPageReqVO extends PageParam {
+
+    @Schema(description = "分组名字", example = "李四")
+    private String name;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 30 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IoT 设备分组 Response VO")
+@Data
+public class IotDeviceGroupRespVO {
+
+    @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583")
+    private Long id;
+
+    @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    private String name;
+
+    @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+    @Schema(description = "分组描述", example = "你说的对")
+    private String description;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+    @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Long deviceCount;
+
+}

+ 26 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group;
+
+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 IotDeviceGroupSaveReqVO {
+
+    @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583")
+    private Long id;
+
+    @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotEmpty(message = "分组名字不能为空")
+    private String name;
+
+    @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "分组状态不能为空")
+    private Integer status;
+
+    @Schema(description = "分组描述", example = "你说的对")
+    private String description;
+
+}

+ 96 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/PluginInfoController.java

@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
+
+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.plugininfo.vo.PluginInfoPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoRespVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
+import cn.iocoder.yudao.module.iot.service.plugininfo.PluginInfoService;
+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 org.springframework.web.multipart.MultipartFile;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
+
+@Tag(name = "管理后台 - IoT 插件信息")
+@RestController
+@RequestMapping("/iot/plugin-info")
+@Validated
+public class PluginInfoController {
+
+    @Resource
+    private PluginInfoService pluginInfoService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建插件信息")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:create')")
+    public CommonResult<Long> createPluginInfo(@Valid @RequestBody PluginInfoSaveReqVO createReqVO) {
+        return success(pluginInfoService.createPluginInfo(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新插件信息")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:update')")
+    public CommonResult<Boolean> updatePluginInfo(@Valid @RequestBody PluginInfoSaveReqVO updateReqVO) {
+        pluginInfoService.updatePluginInfo(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除插件信息")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:delete')")
+    public CommonResult<Boolean> deletePluginInfo(@RequestParam("id") Long id) {
+        pluginInfoService.deletePluginInfo(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得插件信息")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:query')")
+    public CommonResult<PluginInfoRespVO> getPluginInfo(@RequestParam("id") Long id) {
+        PluginInfoDO pluginInfo = pluginInfoService.getPluginInfo(id);
+        return success(BeanUtils.toBean(pluginInfo, PluginInfoRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得插件信息分页")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:query')")
+    public CommonResult<PageResult<PluginInfoRespVO>> getPluginInfoPage(@Valid PluginInfoPageReqVO pageReqVO) {
+        PageResult<PluginInfoDO> pageResult = pluginInfoService.getPluginInfoPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, PluginInfoRespVO.class));
+    }
+
+    @RequestMapping(value = "/update-jar",
+            method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
+    @Operation(summary = "上传Jar包")
+    public CommonResult<Boolean> uploadJar(
+            @RequestParam("id") Long id,
+            @RequestParam("jar") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw exception(FILE_IS_EMPTY);
+        }
+        pluginInfoService.uploadJar(id, file);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "修改插件状态")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-info:update')")
+    public CommonResult<Boolean> updateUserStatus(@Valid @RequestBody PluginInfoSaveReqVO reqVO) {
+        pluginInfoService.updatePluginStatus(reqVO.getId(), reqVO.getStatus());
+        return success(true);
+    }
+
+}

+ 59 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoPageReqVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum;
+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 PluginInfoPageReqVO extends PageParam {
+
+    @Schema(description = "插件包 ID ", example = "24627")
+    private String pluginId;
+
+    @Schema(description = "插件名称", example = "赵六")
+    private String name;
+
+    @Schema(description = "描述", example = "你猜")
+    private String description;
+
+    @Schema(description = "部署方式", example = "2")
+    private Integer deployType;
+
+    @Schema(description = "插件包文件名")
+    private String file;
+
+    @Schema(description = "插件版本")
+    private String version;
+
+    @Schema(description = "插件类型", example = "2")
+    @InEnum(IotPluginTypeEnum.class)
+    private Integer type;
+
+    @Schema(description = "设备插件协议类型")
+    private String protocol;
+
+    @Schema(description = "状态")
+    private Integer status;
+
+    @Schema(description = "插件配置项描述信息")
+    private String configSchema;
+
+    @Schema(description = "插件配置信息")
+    private String config;
+
+    @Schema(description = "插件脚本")
+    private String script;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 71 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoRespVO.java

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IoT 插件信息 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class PluginInfoRespVO {
+
+    @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
+    @ExcelProperty("主键 ID")
+    private Long id;
+
+    @Schema(description = "插件包 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
+    @ExcelProperty("插件包 ID")
+    private String pluginId;
+
+    @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
+    @ExcelProperty("插件名称")
+    private String name;
+
+    @Schema(description = "描述", example = "你猜")
+    @ExcelProperty("描述")
+    private String description;
+
+    @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("部署方式")
+    private Integer deployType;
+
+    @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("插件包文件名")
+    private String file;
+
+    @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("插件版本")
+    private String version;
+
+    @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("插件类型")
+    private Integer type;
+
+    @Schema(description = "设备插件协议类型")
+    @ExcelProperty("设备插件协议类型")
+    private String protocol;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("状态")
+    private Integer status;
+
+    @Schema(description = "插件配置项描述信息")
+    @ExcelProperty("插件配置项描述信息")
+    private String configSchema;
+
+    @Schema(description = "插件配置信息")
+    @ExcelProperty("插件配置信息")
+    private String config;
+
+    @Schema(description = "插件脚本")
+    @ExcelProperty("插件脚本")
+    private String script;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 49 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoSaveReqVO.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Schema(description = "管理后台 - IoT 插件信息新增/修改 Request VO")
+@Data
+public class PluginInfoSaveReqVO {
+
+    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
+    private Long id;
+
+    @Schema(description = "插件包id", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
+    private String pluginId;
+
+    @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 file;
+
+    @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;
+
+}

+ 94 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/PluginInstanceController.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininstance;
+
+import org.springframework.web.bind.annotation.*;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import jakarta.validation.*;
+import jakarta.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.*;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
+import cn.iocoder.yudao.module.iot.service.plugininstance.PluginInstanceService;
+
+@Tag(name = "管理后台 - IoT 插件实例")
+@RestController
+@RequestMapping("/iot/plugin-instance")
+@Validated
+public class PluginInstanceController {
+
+    @Resource
+    private PluginInstanceService pluginInstanceService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建IoT 插件实例")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:create')")
+    public CommonResult<Long> createPluginInstance(@Valid @RequestBody PluginInstanceSaveReqVO createReqVO) {
+        return success(pluginInstanceService.createPluginInstance(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新IoT 插件实例")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:update')")
+    public CommonResult<Boolean> updatePluginInstance(@Valid @RequestBody PluginInstanceSaveReqVO updateReqVO) {
+        pluginInstanceService.updatePluginInstance(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除IoT 插件实例")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:delete')")
+    public CommonResult<Boolean> deletePluginInstance(@RequestParam("id") Long id) {
+        pluginInstanceService.deletePluginInstance(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得IoT 插件实例")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:query')")
+    public CommonResult<PluginInstanceRespVO> getPluginInstance(@RequestParam("id") Long id) {
+        PluginInstanceDO pluginInstance = pluginInstanceService.getPluginInstance(id);
+        return success(BeanUtils.toBean(pluginInstance, PluginInstanceRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得IoT 插件实例分页")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:query')")
+    public CommonResult<PageResult<PluginInstanceRespVO>> getPluginInstancePage(@Valid PluginInstancePageReqVO pageReqVO) {
+        PageResult<PluginInstanceDO> pageResult = pluginInstanceService.getPluginInstancePage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, PluginInstanceRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出IoT 插件实例 Excel")
+    @PreAuthorize("@ss.hasPermission('iot:plugin-instance:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportPluginInstanceExcel(@Valid PluginInstancePageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<PluginInstanceDO> list = pluginInstanceService.getPluginInstancePage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "IoT 插件实例.xls", "数据", PluginInstanceRespVO.class,
+                        BeanUtils.toBean(list, PluginInstanceRespVO.class));
+    }
+
+}

+ 37 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstancePageReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
+
+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:建议搞个 plugin,然后里面分 info 和 instance。另外,是不是 info => config 会好点,插件配置?
+@Schema(description = "管理后台 - IoT 插件实例分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class PluginInstancePageReqVO extends PageParam {
+
+    @Schema(description = "插件主程序id", 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;
+
+}

+ 43 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstanceRespVO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+
+@Schema(description = "管理后台 - IoT 插件实例 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class PluginInstanceRespVO {
+
+    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "23864")
+    @ExcelProperty("主键ID")
+    private Long id;
+
+    @Schema(description = "插件主程序id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738")
+    @ExcelProperty("插件主程序id")
+    private String mainId;
+
+    @Schema(description = "插件id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26498")
+    @ExcelProperty("插件id")
+    private Long pluginId;
+
+    @Schema(description = "插件主程序所在ip", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("插件主程序所在ip")
+    private String ip;
+
+    @Schema(description = "插件主程序端口", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("插件主程序端口")
+    private Integer port;
+
+    @Schema(description = "心跳时间,心路时间超过30秒需要剔除", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("心跳时间,心路时间超过30秒需要剔除")
+    private Long heartbeatAt;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 35 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininstance/vo/PluginInstanceSaveReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - IoT 插件实例新增/修改 Request VO")
+@Data
+public class PluginInstanceSaveReqVO {
+
+    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "23864")
+    private Long id;
+
+    @Schema(description = "插件主程序id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738")
+    @NotEmpty(message = "插件主程序id不能为空")
+    private String mainId;
+
+    @Schema(description = "插件id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26498")
+    @NotNull(message = "插件id不能为空")
+    private Long pluginId;
+
+    @Schema(description = "插件主程序所在ip", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "插件主程序所在ip不能为空")
+    private String ip;
+
+    @Schema(description = "插件主程序端口", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "插件主程序端口不能为空")
+    private Integer port;
+
+    @Schema(description = "心跳时间,心路时间超过30秒需要剔除", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "心跳时间,心路时间超过30秒需要剔除不能为空")
+    private Long heartbeatAt;
+
+}

+ 3 - 3
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java

@@ -121,12 +121,12 @@ public class IotProductController {
     }
 
     @GetMapping("/simple-list")
-    @Operation(summary = "获得所有产品列表")
-    @PreAuthorize("@ss.hasPermission('iot:product:query')")
+    @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
     public CommonResult<List<IotProductRespVO>> getSimpleProductList() {
         List<IotProductDO> list = productService.getProductList();
         return success(convertList(list, product -> // 只返回 id、name 字段
-                new IotProductRespVO().setId(product.getId()).setName(product.getName())));
+                new IotProductRespVO().setId(product.getId()).setName(product.getName())
+                        .setDeviceType(product.getDeviceType())));
     }
 
 }

+ 2 - 2
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java

@@ -38,8 +38,8 @@ public class IotProductRespVO {
     @ExcelProperty("产品图标")
     private String icon;
 
-    @Schema(description = "产品图", example = "https://iocoder.cn/1.png")
-    @ExcelProperty("产品图")
+    @Schema(description = "产品图", example = "https://iocoder.cn/1.png")
+    @ExcelProperty("产品图")
     private String picUrl;
 
     @Schema(description = "产品描述", example = "你猜")

+ 1 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java

@@ -28,7 +28,7 @@ public class IotProductSaveReqVO {
     @Schema(description = "产品图标", example = "https://iocoder.cn/1.svg")
     private String icon;
 
-    @Schema(description = "产品图", example = "https://iocoder.cn/1.png")
+    @Schema(description = "产品图", example = "https://iocoder.cn/1.png")
     private String picUrl;
 
     @Schema(description = "产品描述", example = "描述")

+ 22 - 8
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java

@@ -1,22 +1,25 @@
 package cn.iocoder.yudao.module.iot.dal.dataobject.device;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler;
 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
 import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
 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 java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.Set;
 
 /**
  * IoT 设备 DO
  *
  * @author haohao
  */
-@TableName("iot_device")
+@TableName(value = "iot_device", autoResultMap = true)
 @KeySequence("iot_device_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -47,6 +50,17 @@ public class IotDeviceDO extends BaseDO {
      * 设备序列号
      */
     private String serialNumber;
+    /**
+     * 设备图片
+     */
+    private String picUrl;
+    /**
+     * 设备分组编号集合
+     *
+     * 关联 {@link IotDeviceGroupDO#getId()}
+     */
+    @TableField(typeHandler = LongSetTypeHandler.class)
+    private Set<Long> groupIds;
 
     /**
      * 产品编号
@@ -66,13 +80,6 @@ public class IotDeviceDO extends BaseDO {
      * 冗余 {@link IotProductDO#getDeviceType()}
      */
     private Integer deviceType;
-
-    /**
-     * 设备状态
-     * <p>
-     * 枚举 {@link IotDeviceStatusEnum}
-     */
-    private Integer status;
     /**
      * 网关设备编号
      * <p>
@@ -82,6 +89,13 @@ public class IotDeviceDO extends BaseDO {
      */
     private Long gatewayId;
 
+    /**
+     * 设备状态
+     * <p>
+     * 枚举 {@link IotDeviceStatusEnum}
+     */
+    private Integer status;
+
     /**
      * 设备状态最后更新时间
      */

+ 44 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.iot.dal.dataobject.device;
+
+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.*;
+
+/**
+ * IoT 设备分组 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("iot_device_group")
+@KeySequence("iot_device_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceGroupDO extends BaseDO {
+
+    /**
+     * 分组 ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 分组名字
+     */
+    private String name;
+    /**
+     * 分组状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 分组描述
+     */
+    private String description;
+
+}

+ 83 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugininfo/PluginInfoDO.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo;
+
+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.*;
+
+/**
+ * IoT 插件信息 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("iot_plugin_info")
+@KeySequence("iot_plugin_info_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PluginInfoDO extends BaseDO {
+
+    /**
+     * 主键 ID
+     */
+    @TableId
+    private Long id;
+    // TODO @haohao:这个是不是改成类似 key 之类的字段哈?
+    /**
+     * 插件包 ID
+     */
+    private String pluginId;
+    /**
+     * 插件名称
+     */
+    private String name;
+    /**
+     * 描述
+     */
+    private String description;
+    /**
+     * 部署方式
+     */
+    // TODO @haohao:枚举
+    private Integer deployType;
+    /**
+     * 插件包文件名
+     */
+    // TODO @haohao:是不是叫 fileName 哈?避免后续有别的字段,类似 fileUrl?
+    private String file;
+    /**
+     * 插件版本
+     */
+    private String version;
+    /**
+     * 插件类型
+     */
+    // TODO @haohao:枚举
+    private Integer type;
+    /**
+     * 设备插件协议类型
+     */
+    private String protocol;
+    /**
+     * 状态
+     */
+    // TODO @haohao:枚举
+    private Integer status;
+    /**
+     * 插件配置项描述信息
+     */
+    private String configSchema;
+    /**
+     * 插件配置信息
+     */
+    private String config;
+    /**
+     * 插件脚本
+     */
+    private String script;
+
+}

+ 51 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugininstance/PluginInstanceDO.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance;
+
+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.*;
+
+// TODO @haohao:一些必要的关联、枚举
+/**
+ * IoT 插件实例 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("iot_plugin_instance")
+@KeySequence("iot_plugin_instance_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PluginInstanceDO extends BaseDO {
+
+    /**
+     * 主键ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 插件主程序 ID
+     */
+    private String mainId;
+    /**
+     * 插件id
+     */
+    private Long pluginId;
+    /**
+     * 插件主程序所在ip
+     */
+    private String ip;
+    /**
+     * 插件主程序端口
+     */
+    private Integer port;
+    /**
+     * 心跳时间,心路时间超过 30 秒需要剔除
+     */
+    private Long heartbeatAt;
+
+}

+ 35 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.device;
+
+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.device.vo.group.IotDeviceGroupPageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * IoT 设备分组 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface IotDeviceGroupMapper extends BaseMapperX<IotDeviceGroupDO> {
+
+    default PageResult<IotDeviceGroupDO> selectPage(IotDeviceGroupPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceGroupDO>()
+                .likeIfPresent(IotDeviceGroupDO::getName, reqVO.getName())
+                .betweenIfPresent(IotDeviceGroupDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(IotDeviceGroupDO::getId));
+    }
+
+    default List<IotDeviceGroupDO> selectListByStatus(Integer status) {
+        return selectList(IotDeviceGroupDO::getStatus, status);
+    }
+
+    default IotDeviceGroupDO selectByName(String name) {
+        return selectOne(IotDeviceGroupDO::getName, name);
+    }
+
+}

+ 23 - 14
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.iot.dal.mysql.device;
 
+import cn.hutool.core.util.ObjectUtil;
 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;
@@ -7,6 +8,8 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa
 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 /**
  * IoT 设备 Mapper
  *
@@ -15,30 +18,21 @@ import org.apache.ibatis.annotations.Mapper;
 @Mapper
 public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
 
-    // TODO @haohao:可能多余的查询条件,要去掉哈
     default PageResult<IotDeviceDO> selectPage(IotDevicePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceDO>()
-                .eqIfPresent(IotDeviceDO::getDeviceKey, reqVO.getDeviceKey())
                 .likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName())
                 .eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId())
-                .eqIfPresent(IotDeviceDO::getProductKey, reqVO.getProductKey())
                 .eqIfPresent(IotDeviceDO::getDeviceType, reqVO.getDeviceType())
                 .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname())
-                .eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId())
                 .eqIfPresent(IotDeviceDO::getStatus, reqVO.getStatus())
-                .betweenIfPresent(IotDeviceDO::getStatusLastUpdateTime, reqVO.getStatusLastUpdateTime())
-                .betweenIfPresent(IotDeviceDO::getLastOnlineTime, reqVO.getLastOnlineTime())
-                .betweenIfPresent(IotDeviceDO::getLastOfflineTime, reqVO.getLastOfflineTime())
-                .betweenIfPresent(IotDeviceDO::getActiveTime, reqVO.getActiveTime())
-                .eqIfPresent(IotDeviceDO::getDeviceSecret, reqVO.getDeviceSecret())
-                .eqIfPresent(IotDeviceDO::getMqttClientId, reqVO.getMqttClientId())
-                .likeIfPresent(IotDeviceDO::getMqttUsername, reqVO.getMqttUsername())
-                .eqIfPresent(IotDeviceDO::getMqttPassword, reqVO.getMqttPassword())
-                .eqIfPresent(IotDeviceDO::getAuthType, reqVO.getAuthType())
-                .betweenIfPresent(IotDeviceDO::getCreateTime, reqVO.getCreateTime())
+                .apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0")
                 .orderByDesc(IotDeviceDO::getId));
     }
 
+    default IotDeviceDO selectByDeviceName(String deviceName) {
+        return selectOne(IotDeviceDO::getDeviceName, deviceName);
+    }
+
     default IotDeviceDO selectByProductKeyAndDeviceName(String productKey, String deviceName) {
         return selectOne(IotDeviceDO::getProductKey, productKey,
                 IotDeviceDO::getDeviceName, deviceName);
@@ -51,4 +45,19 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
     default Long selectCountByProductId(Long productId) {
         return selectCount(IotDeviceDO::getProductId, productId);
     }
+
+    default IotDeviceDO selectByDeviceKey(String deviceKey) {
+        return selectOne(IotDeviceDO::getDeviceKey, deviceKey);
+    }
+
+    default List<IotDeviceDO> selectList(Integer deviceType) {
+        return selectList(IotDeviceDO::getDeviceType, deviceType);
+    }
+
+    default Long selectCountByGroupId(Long groupId) {
+        return selectCount(new LambdaQueryWrapperX<IotDeviceDO>()
+                .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0")
+                .orderByDesc(IotDeviceDO::getId));
+    }
+
 }

+ 36 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugininfo/PluginInfoMapper.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.plugininfo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.*;
+
+/**
+ * IoT 插件信息 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface PluginInfoMapper extends BaseMapperX<PluginInfoDO> {
+
+    default PageResult<PluginInfoDO> selectPage(PluginInfoPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<PluginInfoDO>()
+                .eqIfPresent(PluginInfoDO::getPluginId, reqVO.getPluginId())
+                .likeIfPresent(PluginInfoDO::getName, reqVO.getName())
+                .eqIfPresent(PluginInfoDO::getDescription, reqVO.getDescription())
+                .eqIfPresent(PluginInfoDO::getDeployType, reqVO.getDeployType())
+                .eqIfPresent(PluginInfoDO::getFile, reqVO.getFile())
+                .eqIfPresent(PluginInfoDO::getVersion, reqVO.getVersion())
+                .eqIfPresent(PluginInfoDO::getType, reqVO.getType())
+                .eqIfPresent(PluginInfoDO::getProtocol, reqVO.getProtocol())
+                .eqIfPresent(PluginInfoDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(PluginInfoDO::getConfigSchema, reqVO.getConfigSchema())
+                .eqIfPresent(PluginInfoDO::getConfig, reqVO.getConfig())
+                .eqIfPresent(PluginInfoDO::getScript, reqVO.getScript())
+                .betweenIfPresent(PluginInfoDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(PluginInfoDO::getId));
+    }
+
+}

+ 29 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugininstance/PluginInstanceMapper.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.plugininstance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.*;
+
+/**
+ * IoT 插件实例 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface PluginInstanceMapper extends BaseMapperX<PluginInstanceDO> {
+
+    default PageResult<PluginInstanceDO> selectPage(PluginInstancePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<PluginInstanceDO>()
+                .eqIfPresent(PluginInstanceDO::getMainId, reqVO.getMainId())
+                .eqIfPresent(PluginInstanceDO::getPluginId, reqVO.getPluginId())
+                .eqIfPresent(PluginInstanceDO::getIp, reqVO.getIp())
+                .eqIfPresent(PluginInstanceDO::getPort, reqVO.getPort())
+                .eqIfPresent(PluginInstanceDO::getHeartbeatAt, reqVO.getHeartbeatAt())
+                .betweenIfPresent(PluginInstanceDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(PluginInstanceDO::getId));
+    }
+
+}

+ 1 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java

@@ -21,7 +21,7 @@ public interface IotProductCategoryMapper extends BaseMapperX<IotProductCategory
         return selectPage(reqVO, new LambdaQueryWrapperX<IotProductCategoryDO>()
                 .likeIfPresent(IotProductCategoryDO::getName, reqVO.getName())
                 .betweenIfPresent(IotProductCategoryDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(IotProductCategoryDO::getId));
+                .orderByAsc(IotProductCategoryDO::getSort));
     }
 
     default List<IotProductCategoryDO> selectListByStatus(Integer status) {

+ 1 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java

@@ -23,7 +23,7 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> {
     }
 
     default IotProductDO selectByProductKey(String productKey) {
-        return selectOne(new LambdaQueryWrapperX<IotProductDO>().eq(IotProductDO::getProductKey, productKey));
+        return selectOne(IotProductDO::getProductKey, productKey);
     }
 
 }

+ 15 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/SpringConfiguration.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.iot.framework.plugin;
+
+import org.pf4j.spring.SpringPluginManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringConfiguration {
+
+    @Bean
+    public SpringPluginManager pluginManager() {
+        return new SpringPluginManager();
+    }
+
+}

+ 0 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceDataService.java

@@ -15,7 +15,6 @@ import java.util.Map;
  */
 public interface IotDeviceDataService {
 
-
     /**
      * 保存设备数据
      *

+ 96 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java

@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.iot.service.device;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * IoT 设备分组 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface IotDeviceGroupService {
+
+    /**
+     * 创建设备分组
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createDeviceGroup(@Valid IotDeviceGroupSaveReqVO createReqVO);
+
+    /**
+     * 更新设备分组
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDeviceGroup(@Valid IotDeviceGroupSaveReqVO updateReqVO);
+
+    /**
+     * 删除设备分组
+     *
+     * @param id 编号
+     */
+    void deleteDeviceGroup(Long id);
+
+    /**
+     * 校验设备分组是否存在
+     *
+     * @param ids 设备分组 ID 数组
+     */
+    default List<IotDeviceGroupDO> validateDeviceGroupExists(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return List.of();
+        }
+        return convertList(ids, this::validateDeviceGroupExists);
+    }
+
+    /**
+     * 校验设备分组是否存在
+     *
+     * @param id 设备分组 ID
+     * @return 设备分组
+     */
+    IotDeviceGroupDO validateDeviceGroupExists(Long id);
+
+    /**
+     * 获得设备分组
+     *
+     * @param id 编号
+     * @return 设备分组
+     */
+    IotDeviceGroupDO getDeviceGroup(Long id);
+
+    /**
+     * 获得设备分组
+     *
+     * @param name 名称
+     * @return 设备分组
+     */
+    IotDeviceGroupDO getDeviceGroupByName(String name);
+
+    /**
+     * 获得设备分组分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 设备分组分页
+     */
+    PageResult<IotDeviceGroupDO> getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO);
+
+    /**
+     * 获得设备分组列表
+     *
+     * @param status 状态
+     * @return 设备分组列表
+     */
+    List<IotDeviceGroupDO> getDeviceGroupListByStatus(Integer status);
+
+}

+ 94 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.iot.service.device;
+
+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.group.IotDeviceGroupPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceGroupMapper;
+import jakarta.annotation.Resource;
+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.DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_GROUP_NOT_EXISTS;
+
+/**
+ * IoT 设备分组 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class IotDeviceGroupServiceImpl implements IotDeviceGroupService {
+
+    @Resource
+    private IotDeviceGroupMapper deviceGroupMapper;
+
+    @Resource
+    private IotDeviceService deviceService;
+
+    @Override
+    public Long createDeviceGroup(IotDeviceGroupSaveReqVO createReqVO) {
+        // 插入
+        IotDeviceGroupDO deviceGroup = BeanUtils.toBean(createReqVO, IotDeviceGroupDO.class);
+        deviceGroupMapper.insert(deviceGroup);
+        // 返回
+        return deviceGroup.getId();
+    }
+
+    @Override
+    public void updateDeviceGroup(IotDeviceGroupSaveReqVO updateReqVO) {
+        // 校验存在
+        validateDeviceGroupExists(updateReqVO.getId());
+        // 更新
+        IotDeviceGroupDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceGroupDO.class);
+        deviceGroupMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteDeviceGroup(Long id) {
+        // 1.1 校验存在
+        validateDeviceGroupExists(id);
+        // 1.2 校验是否存在设备
+        if (deviceService.getDeviceCountByGroupId(id) > 0) {
+            throw exception(DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS);
+        }
+
+        // 删除
+        deviceGroupMapper.deleteById(id);
+    }
+
+    @Override
+    public IotDeviceGroupDO validateDeviceGroupExists(Long id) {
+        IotDeviceGroupDO group = deviceGroupMapper.selectById(id);
+        if (group == null) {
+            throw exception(DEVICE_GROUP_NOT_EXISTS);
+        }
+        return group;
+    }
+
+    @Override
+    public IotDeviceGroupDO getDeviceGroup(Long id) {
+        return deviceGroupMapper.selectById(id);
+    }
+
+    @Override
+    public IotDeviceGroupDO getDeviceGroupByName(String name) {
+        return deviceGroupMapper.selectByName(name);
+    }
+
+    @Override
+    public PageResult<IotDeviceGroupDO> getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO) {
+        return deviceGroupMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<IotDeviceGroupDO> getDeviceGroupListByStatus(Integer status) {
+        return deviceGroupMapper.selectListByStatus(status);
+    }
+
+}

+ 54 - 7
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java

@@ -1,11 +1,18 @@
 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.IotDevicePageReqVO;
 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
-import jakarta.validation.*;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceUpdateGroupReqVO;
 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceImportRespVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceImportExcelVO;
+import jakarta.validation.Valid;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * IoT 设备 Service 接口
@@ -30,12 +37,33 @@ public interface IotDeviceService {
     void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
 
     /**
-     * 删除设备
+     * 更新设备状态
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO);
+
+    /**
+     * 更新设备分组
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO);
+
+    /**
+     * 删除单个设备
      *
      * @param id 编号
      */
     void deleteDevice(Long id);
 
+    /**
+     * 删除多个设备
+     *
+     * @param ids 编号数组
+     */
+    void deleteDeviceList(Collection<Long> ids);
+
     /**
      * 获得设备
      *
@@ -45,7 +73,7 @@ public interface IotDeviceService {
     IotDeviceDO getDevice(Long id);
 
     /**
-     * 得设备分页
+     * ��得设备分页
      *
      * @param pageReqVO 分页查询
      * @return IoT 设备分页
@@ -53,11 +81,12 @@ public interface IotDeviceService {
     PageResult<IotDeviceDO> getDevicePage(IotDevicePageReqVO pageReqVO);
 
     /**
-     * 更新设备状态
+     * 获得设备列表
      *
-     * @param updateReqVO 更新信息
+     * @param deviceType 设备类型
+     * @return 设备列表
      */
-    void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO);
+    List<IotDeviceDO> getDeviceList(@Nullable Integer deviceType);
 
     /**
      * 获得设备数量
@@ -67,6 +96,14 @@ public interface IotDeviceService {
      */
     Long getDeviceCountByProductId(Long productId);
 
+    /**
+     * 获得设备数量
+     *
+     * @param groupId 分组编号
+     * @return 设备数量
+     */
+    Long getDeviceCountByGroupId(Long groupId);
+
     /**
      * 根据产品 key 和设备名称,获得设备信息
      *
@@ -75,4 +112,14 @@ public interface IotDeviceService {
      * @return 设备信息
      */
     IotDeviceDO getDeviceByProductKeyAndDeviceName(String productKey, String deviceName);
+
+    /**
+     * 导入设备
+     *
+     * @param importDevices 导入设备列表
+     * @param updateSupport 是否支持更新
+     * @return 导入结果
+     */
+    IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport);
+
 }

+ 234 - 127
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java

@@ -1,31 +1,36 @@
 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.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
+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.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.enums.device.IotDeviceStatusEnum;
+import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
 import jakarta.annotation.Resource;
+import jakarta.validation.ConstraintViolationException;
 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.security.SecureRandom;
+import javax.annotation.Nullable;
 import java.time.LocalDateTime;
-import java.util.Base64;
-import java.util.Objects;
-import java.util.UUID;
+import java.util.*;
 
 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.*;
 
 /**
@@ -40,142 +45,87 @@ public class IotDeviceServiceImpl implements IotDeviceService {
 
     @Resource
     private IotDeviceMapper deviceMapper;
+
     @Resource
     private IotProductService productService;
+    @Resource
+    @Lazy // 延迟加载,解决循环依赖
+    private IotDeviceGroupService deviceGroupService;
 
-    /**
-     * 创建 IoT 设备
-     *
-     * @param createReqVO 创建请求 VO
-     * @return 设备 ID
-     */
     @Override
-    @Transactional(rollbackFor = Exception.class)
     public Long createDevice(IotDeviceSaveReqVO createReqVO) {
         // 1.1 校验产品是否存在
         IotProductDO product = productService.getProduct(createReqVO.getProductId());
         if (product == null) {
             throw exception(PRODUCT_NOT_EXISTS);
         }
-        // 1.2 校验设备名称在同一产品下是否唯一
-        if (StrUtil.isBlank(createReqVO.getDeviceName())) {
-            createReqVO.setDeviceName(generateUniqueDeviceName(product.getProductKey()));
-        } else {
-            validateDeviceNameUnique(product.getProductKey(), createReqVO.getDeviceName());
+        // 1.2 校验设备标识是否唯一
+        if (deviceMapper.selectByDeviceKey(createReqVO.getDeviceKey()) != null) {
+            throw exception(DEVICE_KEY_EXISTS);
+        }
+        // 1.3 校验设备名称在同一产品下是否唯一
+        if (deviceMapper.selectByProductKeyAndDeviceName(product.getProductKey(), createReqVO.getDeviceKey()) != null) {
+            throw exception(DEVICE_NAME_EXISTS);
         }
+        // 1.4 校验父设备是否为合法网关
+        if (IotProductDeviceTypeEnum.isGateway(product.getDeviceType())
+                && createReqVO.getGatewayId() != null) {
+            validateGatewayDeviceExists(createReqVO.getGatewayId());
+        }
+        // 1.5 校验分组存在
+        deviceGroupService.validateDeviceGroupExists(createReqVO.getGroupIds());
 
         // 2.1 转换 VO 为 DO
-        IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class)
-                .setProductKey(product.getProductKey())
-                .setDeviceType(product.getDeviceType());
-        // 2.2 生成并设置必要的字段
-        device.setDeviceKey(generateUniqueDeviceKey());
-        device.setDeviceSecret(generateDeviceSecret());
-        device.setMqttClientId(generateMqttClientId());
-        device.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey()));
-        device.setMqttPassword(generateMqttPassword());
-        // 2.3 设置设备状态为未激活
-        device.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus());
-        device.setStatusLastUpdateTime(LocalDateTime.now());
-        // 2.4 插入到数据库
+        IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class, o -> {
+            o.setProductKey(product.getProductKey()).setDeviceType(product.getDeviceType());
+            // 生成并设置必要的字段
+            o.setDeviceSecret(generateDeviceSecret())
+                    .setMqttClientId(generateMqttClientId())
+                    .setMqttUsername(generateMqttUsername(o.getDeviceName(), o.getProductKey()))
+                    .setMqttPassword(generateMqttPassword());
+            // 设置设备状态为未激活
+            o.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus()).setStatusLastUpdateTime(LocalDateTime.now());
+        });
+        // 2.2 插入到数据库
         deviceMapper.insert(device);
         return device.getId();
     }
 
-    /**
-     * 校验设备名称在同一产品下是否唯一
-     *
-     * @param productKey 产品 Key
-     * @param deviceName 设备名称
-     */
-    private void validateDeviceNameUnique(String productKey, String deviceName) {
-        IotDeviceDO existingDevice = deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName);
-        if (existingDevice != null) {
-            throw exception(DEVICE_NAME_EXISTS);
+    @Override
+    public void updateDevice(IotDeviceSaveReqVO updateReqVO) {
+        updateReqVO.setDeviceKey(null).setDeviceName(null).setProductId(null); // 不允许更新
+        // 1.1 校验存在
+        IotDeviceDO device = validateDeviceExists(updateReqVO.getId());
+        // 1.2 校验父设备是否为合法网关
+        if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType())
+                && updateReqVO.getGatewayId() != null) {
+            validateGatewayDeviceExists(updateReqVO.getGatewayId());
         }
-    }
+        // 1.3 校验分组存在
+        deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds());
 
-    /**
-     * 生成唯一的 deviceKey
-     *
-     * @return 生成的 deviceKey
-     */
-    private String generateUniqueDeviceKey() {
-        return UUID.randomUUID().toString();
-    }
-
-    /**
-     * 生成 deviceSecret
-     *
-     * @return 生成的 deviceSecret
-     */
-    private String generateDeviceSecret() {
-        return IdUtil.fastSimpleUUID();
-    }
-
-    /**
-     * 生成 MQTT Client ID
-     *
-     * @return 生成的 MQTT Client ID
-     */
-    private String generateMqttClientId() {
-        return UUID.randomUUID().toString();
-    }
-
-    /**
-     * 生成 MQTT Username
-     *
-     * @param deviceName 设备名称
-     * @param productKey 产品 Key
-     * @return 生成的 MQTT Username
-     */
-    private String generateMqttUsername(String deviceName, String productKey) {
-        return deviceName + "&" + productKey;
-    }
-
-    /**
-     * 生成 MQTT Password
-     *
-     * @return 生成的 MQTT Password
-     */
-    private String generateMqttPassword() {
-        // TODO @浩浩:这里的 StrUtil 随机字符串?
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] passwordBytes = new byte[32]; // 256 位的随机数
-        secureRandom.nextBytes(passwordBytes);
-        return Base64.getUrlEncoder().withoutPadding().encodeToString(passwordBytes);
-    }
-
-    /**
-     * 生成唯一的 DeviceName
-     *
-     * @param productKey 产品标识
-     * @return 生成的唯一 DeviceName
-     */
-    private String generateUniqueDeviceName(String productKey) {
-        for (int i = 0; i < Short.MAX_VALUE; i++) {
-            String deviceName = IdUtil.fastSimpleUUID().substring(0, 20);
-            if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) {
-                return deviceName;
-            }
-        }
-        throw new IllegalArgumentException("生成 DeviceName 失败");
+        // 2. 更新到数据库
+        IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class);
+        deviceMapper.updateById(updateObj);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void updateDevice(IotDeviceSaveReqVO updateReqVO) {
-        // 1. 校验存在
-        validateDeviceExists(updateReqVO.getId());
+    public void updateDeviceGroup(IotDeviceUpdateGroupReqVO updateReqVO) {
+        // 1.1 校验设备存在
+        List<IotDeviceDO> devices = deviceMapper.selectBatchIds(updateReqVO.getIds());
+        if (CollUtil.isEmpty(devices)) {
+            return;
+        }
+        // 1.2 校验分组存在
+        deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds());
 
-        // 2. 更新到数据库
-        IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class)
-                .setDeviceName(null).setProductId(null); // 设备名称 和 产品 ID 不能修改
-        deviceMapper.updateById(updateObj);
+        // 3. 更新设备分组
+        deviceMapper.updateBatch(convertList(devices, device -> new IotDeviceDO()
+                .setId(device.getId()).setGroupIds(updateReqVO.getGroupIds())));
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
     public void deleteDevice(Long id) {
         // 1.1 校验存在
         IotDeviceDO device = validateDeviceExists(id);
@@ -188,6 +138,28 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         deviceMapper.deleteById(id);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDeviceList(Collection<Long> ids) {
+        // 1.1 校验存在
+        if (CollUtil.isEmpty(ids)) {
+            return;
+        }
+        List<IotDeviceDO> devices = deviceMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(devices)) {
+            return;
+        }
+        // 1.2 校验网关设备是否存在
+        for (IotDeviceDO device : devices) {
+            if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(device.getId()) > 0) {
+                throw exception(DEVICE_HAS_CHILDREN);
+            }
+        }
+
+        // 2. 删除设备
+        deviceMapper.deleteByIds(ids);
+    }
+
     /**
      * 校验设备是否存在
      *
@@ -202,13 +174,24 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         return device;
     }
 
-    @Override
-    public IotDeviceDO getDevice(Long id) {
+    /**
+     * 校验网关设备是否存在
+     *
+     * @param id 设备 ID
+     */
+    private void validateGatewayDeviceExists(Long id) {
         IotDeviceDO device = deviceMapper.selectById(id);
         if (device == null) {
-            throw exception(DEVICE_NOT_EXISTS);
+            throw exception(DEVICE_GATEWAY_NOT_EXISTS);
         }
-        return device;
+        if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
+            throw exception(DEVICE_NOT_GATEWAY);
+        }
+    }
+
+    @Override
+    public IotDeviceDO getDevice(Long id) {
+        return deviceMapper.selectById(id);
     }
 
     @Override
@@ -216,19 +199,24 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         return deviceMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public List<IotDeviceDO> getDeviceList(@Nullable Integer deviceType) {
+        return deviceMapper.selectList(deviceType);
+    }
+
     @Override
     public void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO) {
         // 1. 校验存在
         IotDeviceDO device = validateDeviceExists(updateReqVO.getId());
 
         // 2.1 更新状态和更新时间
-        IotDeviceDO updateDevice = BeanUtils.toBean(updateReqVO, IotDeviceDO.class);
+        IotDeviceDO updateDevice = BeanUtils.toBean(updateReqVO, IotDeviceDO.class)
+                .setStatusLastUpdateTime(LocalDateTime.now());
         // 2.2 更新状态相关时间
         if (Objects.equals(device.getStatus(), IotDeviceStatusEnum.INACTIVE.getStatus())
                 && Objects.equals(updateDevice.getStatus(), IotDeviceStatusEnum.ONLINE.getStatus())) {
             // 从未激活到在线,设置激活时间和最后上线时间
-            updateDevice.setActiveTime(LocalDateTime.now());
-            updateDevice.setLastOnlineTime(LocalDateTime.now());
+            updateDevice.setActiveTime(LocalDateTime.now()).setLastOnlineTime(LocalDateTime.now());
         } else if (Objects.equals(updateDevice.getStatus(), IotDeviceStatusEnum.ONLINE.getStatus())) {
             // 如果是上线,设置最后上线时间
             updateDevice.setLastOnlineTime(LocalDateTime.now());
@@ -236,10 +224,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
             // 如果是离线,设置最后离线时间
             updateDevice.setLastOfflineTime(LocalDateTime.now());
         }
-
-        // 2.3 设置状态更新时间
-        updateDevice.setStatusLastUpdateTime(LocalDateTime.now());
-        // 2.4 更新到数据库
+        // 2.3 更新到数据库
         deviceMapper.updateById(updateDevice);
     }
 
@@ -248,10 +233,132 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         return deviceMapper.selectCountByProductId(productId);
     }
 
+    @Override
+    public Long getDeviceCountByGroupId(Long groupId) {
+        return deviceMapper.selectCountByGroupId(groupId);
+    }
+
     @Override
     @TenantIgnore
     public IotDeviceDO getDeviceByProductKeyAndDeviceName(String productKey, String deviceName) {
         return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName);
     }
 
+    /**
+     * 生成 deviceKey
+     *
+     * @return 生成的 deviceKey
+     */
+    private String generateDeviceKey() {
+        return RandomUtil.randomString(16);
+    }
+
+    /**
+     * 生成 deviceSecret
+     *
+     * @return 生成的 deviceSecret
+     */
+    private String generateDeviceSecret() {
+        return IdUtil.fastSimpleUUID();
+    }
+
+    /**
+     * 生成 MQTT Client ID
+     *
+     * @return 生成的 MQTT Client ID
+     */
+    private String generateMqttClientId() {
+        return IdUtil.fastSimpleUUID();
+    }
+
+    /**
+     * 生成 MQTT Username
+     *
+     * @param deviceName 设备名称
+     * @param productKey 产品 Key
+     * @return 生成的 MQTT Username
+     */
+    private String generateMqttUsername(String deviceName, String productKey) {
+        return deviceName + "&" + productKey;
+    }
+
+    /**
+     * 生成 MQTT Password
+     *
+     * @return 生成的 MQTT Password
+     */
+    private String generateMqttPassword() {
+        return RandomUtil.randomString(32);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
+    public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
+        // 1. 参数校验
+        if (CollUtil.isEmpty(importDevices)) {
+            throw exception(DEVICE_IMPORT_LIST_IS_EMPTY);
+        }
+
+        // 2. 遍历,逐个创建 or 更新
+        IotDeviceImportRespVO respVO = IotDeviceImportRespVO.builder().createDeviceNames(new ArrayList<>())
+                .updateDeviceNames(new ArrayList<>()).failureDeviceNames(new LinkedHashMap<>()).build();
+        importDevices.forEach(importDevice -> {
+            try {
+                // 2.1.1 校验字段是否符合要求
+                try {
+                    ValidationUtils.validate(importDevice);
+                } catch (ConstraintViolationException ex){
+                    respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage());
+                    return;
+                }
+                // 2.1.2 校验产品是否存在
+                IotProductDO product = productService.validateProductExists(importDevice.getProductKey());
+                // 2.1.3 校验父设备是否存在
+                Long gatewayId = null;
+                if (StrUtil.isNotEmpty(importDevice.getParentDeviceName())) {
+                    IotDeviceDO gatewayDevice = deviceMapper.selectByDeviceName(importDevice.getParentDeviceName());
+                    if (gatewayDevice == null) {
+                        throw exception(DEVICE_GATEWAY_NOT_EXISTS);
+                    }
+                    if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
+                        throw exception(DEVICE_NOT_GATEWAY);
+                    }
+                    gatewayId = gatewayDevice.getId();
+                }
+                // 2.1.4 校验设备分组是否存在
+                Set<Long> groupIds = new HashSet<>();
+                if (StrUtil.isNotEmpty(importDevice.getGroupNames())) {
+                    String[] groupNames = importDevice.getGroupNames().split(",");
+                    for (String groupName : groupNames) {
+                        IotDeviceGroupDO group = deviceGroupService.getDeviceGroupByName(groupName);
+                        if (group == null) {
+                            throw exception(DEVICE_GROUP_NOT_EXISTS);
+                        }
+                        groupIds.add(group.getId());
+                    }
+                }
+
+                // 2.2.1 判断如果不存在,在进行插入
+                IotDeviceDO existDevice = deviceMapper.selectByDeviceName(importDevice.getDeviceName());
+                if (existDevice == null) {
+                    createDevice(new IotDeviceSaveReqVO()
+                            .setDeviceName(importDevice.getDeviceName()).setDeviceKey(generateDeviceKey())
+                            .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds));
+                    respVO.getCreateDeviceNames().add(importDevice.getDeviceName());
+                    return;
+                }
+                // 2.2.2 如果存在,判断是否允许更新
+                if (updateSupport) {
+                    throw exception(DEVICE_KEY_EXISTS);
+                }
+                updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId())
+                        .setGatewayId(gatewayId).setGroupIds(groupIds));
+                respVO.getUpdateDeviceNames().add(importDevice.getDeviceName());
+            } catch (ServiceException ex) {
+                respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage());
+            }
+        });
+        return respVO;
+    }
+
 }

+ 72 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoService.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.iot.service.plugininfo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
+import jakarta.validation.Valid;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+
+/**
+ * IoT 插件信息 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PluginInfoService {
+
+    /**
+     * 创建IoT 插件信息
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createPluginInfo(@Valid PluginInfoSaveReqVO createReqVO);
+
+    /**
+     * 更新IoT 插件信息
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePluginInfo(@Valid PluginInfoSaveReqVO updateReqVO);
+
+    /**
+     * 删除IoT 插件信息
+     *
+     * @param id 编号
+     */
+    void deletePluginInfo(Long id);
+
+    /**
+     * 获得IoT 插件信息
+     *
+     * @param id 编号
+     * @return IoT 插件信息
+     */
+    PluginInfoDO getPluginInfo(Long id);
+
+    /**
+     * 获得IoT 插件信息分页
+     *
+     * @param pageReqVO 分页查询
+     * @return IoT 插件信息分页
+     */
+    PageResult<PluginInfoDO> getPluginInfoPage(PluginInfoPageReqVO pageReqVO);
+
+    /**
+     * 上传插件的 JAR 包
+     *
+     * @param id 插件id
+     * @param file 文件
+     */
+    void uploadJar(Long id, MultipartFile file);
+
+    /**
+     * 更新插件的状态
+     *
+     * @param id 插件id
+     * @param status 状态
+     */
+    void updatePluginStatus(Long id, Integer status);
+}

+ 267 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoServiceImpl.java

@@ -0,0 +1,267 @@
+package cn.iocoder.yudao.module.iot.service.plugininfo;
+
+import cn.hutool.core.io.IoUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.infra.api.file.FileApi;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper;
+import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.pf4j.PluginDescriptor;
+import org.pf4j.PluginState;
+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.nio.file.Path;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
+
+/**
+ * IoT 插件信息 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+@Slf4j
+public class PluginInfoServiceImpl implements PluginInfoService {
+
+    @Resource
+    private PluginInfoMapper pluginInfoMapper;
+
+    @Resource
+    private SpringPluginManager pluginManager;
+
+    @Resource
+    private FileApi fileApi;
+
+    @Override
+    public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
+        // 插入
+        PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class);
+        pluginInfoMapper.insert(pluginInfo);
+        // 返回
+        return pluginInfo.getId();
+    }
+
+    @Override
+    public void updatePluginInfo(PluginInfoSaveReqVO updateReqVO) {
+        // 校验存在
+        validatePluginInfoExists(updateReqVO.getId());
+        // 更新
+        PluginInfoDO updateObj = BeanUtils.toBean(updateReqVO, PluginInfoDO.class);
+        pluginInfoMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deletePluginInfo(Long id) {
+        // 校验存在
+        PluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
+
+        // 停止插件
+        if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
+            throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
+        }
+
+        // 卸载插件
+        PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginId());
+        if (plugin != null) {
+            // 查询插件是否是启动状态
+            if (plugin.getPluginState().equals(PluginState.STARTED)) {
+                // 停止插件
+                pluginManager.stopPlugin(plugin.getPluginId());
+            }
+            // 卸载插件
+            pluginManager.unloadPlugin(plugin.getPluginId());
+        }
+
+        // 删除
+        pluginInfoMapper.deleteById(id);
+    }
+
+    private PluginInfoDO validatePluginInfoExists(Long id) {
+        PluginInfoDO pluginInfo = pluginInfoMapper.selectById(id);
+        if (pluginInfo == null) {
+            throw exception(PLUGIN_INFO_NOT_EXISTS);
+        }
+        return pluginInfo;
+    }
+
+    @Override
+    public PluginInfoDO getPluginInfo(Long id) {
+        return pluginInfoMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<PluginInfoDO> getPluginInfoPage(PluginInfoPageReqVO pageReqVO) {
+        return pluginInfoMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public void uploadJar(Long id, MultipartFile file) {
+        // 1. 校验存在
+        PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
+
+        // 2. 判断文件名称与插件 ID 是否匹配
+        String pluginId = pluginInfoDo.getPluginId();
+
+        // 3. 停止卸载旧的插件
+        // 3.1. 获取插件信息
+        PluginWrapper plugin = pluginManager.getPlugin(pluginId);
+        if (plugin != null) {
+            // 3.2. 如果插件状态是启动的,停止插件
+            if (plugin.getPluginState().equals(PluginState.STARTED)) {
+                pluginManager.stopPlugin(pluginId);
+            }
+            // 3.3. 卸载插件
+            pluginManager.unloadPlugin(pluginId);
+        }
+
+        // 4. 上传插件
+        String pluginIdNew;
+        try {
+            String path = fileApi.createFile(IoUtil.readBytes(file.getInputStream()));
+            Path pluginPath = Path.of(path);
+            pluginIdNew = pluginManager.loadPlugin(pluginPath);
+        } catch (Exception e) {
+            throw exception(PLUGIN_INSTALL_FAILED);
+        }
+
+        PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);
+        if (pluginWrapper == null) {
+            throw exception(PLUGIN_INSTALL_FAILED);
+        }
+
+
+        // 5. 读取配置文件和脚本
+        String configJson = "";
+        String script = "";
+
+        pluginInfoDo.setPluginId(pluginIdNew);
+        pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
+        pluginInfoDo.setFile(file.getOriginalFilename());
+        pluginInfoDo.setConfigSchema(configJson);
+        pluginInfoDo.setScript(script);
+
+        PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
+        pluginInfoDo.setVersion(pluginDescriptor.getVersion());
+        pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
+        pluginInfoMapper.updateById(pluginInfoDo);
+
+
+        // 5. 读取配置文件和脚本
+//        String configJson = "";
+        //       String script = "";
+//        try (JarFile jarFile = new JarFile(pluginInfoUpdate.getPluginPath())) {
+//            // 5.1 获取config文件在jar包中的路径
+//            String configFile = "classes/config.json";
+//            JarEntry configEntry = jarFile.getJarEntry(configFile);
+//
+//            if (configEntry != null) {
+//                // 5.2 读取配置文件
+//                configJson = IoUtil.read(jarFile.getInputStream(configEntry), Charset.defaultCharset());
+//                log.info("configJson:{}", configJson);
+//            }
+//
+//            // 5.3 读取script.js脚本
+//            String scriptFile = "classes/script.js";
+//            JarEntry scriptEntity = jarFile.getJarEntry(scriptFile);
+//            if (scriptEntity != null) {
+//                // 5.4 读取脚本文件
+//                script = IoUtil.read(jarFile.getInputStream(scriptEntity), Charset.defaultCharset());
+//                log.info("script:{}", script);
+//            }
+//        } catch (Exception e) {
+//            throw exception(PLUGIN_INSTALL_FAILED);
+//        }
+
+
+//        PluginState pluginState = pluginInfoUpdate.getPluginState();
+//        if (pluginState == PluginState.STARTED) {
+//            pluginInfoDo.setStatus(IotPluginStatusEnum.RUNNING.getStatus());
+//        }
+//        pluginInfoDo.setPluginId(pluginInfoUpdate.getPluginId());
+//        pluginInfoDo.setFile(file.getOriginalFilename());
+//        pluginInfoDo.setConfigSchema(configJson);
+//        pluginInfoDo.setScript(script);
+//
+//        PluginDescriptor pluginDescriptor = pluginInfoUpdate.getPluginDescriptor();
+//        pluginInfoDo.setVersion(pluginDescriptor.getPluginVersion());
+//        pluginInfoDo.setDescription(pluginDescriptor.getDescription());
+//        pluginInfoMapper.updateById(pluginInfoDo);
+    }
+
+    @Override
+    public void updatePluginStatus(Long id, Integer status) {
+        // 1. 校验存在
+        PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
+
+        // 插件状态无效
+        if (!IotPluginStatusEnum.contains(status)) {
+            throw exception(PLUGIN_STATUS_INVALID);
+        }
+
+        // 插件包为空
+//        String pluginId = pluginInfoDo.getPluginId();
+//        if (StrUtil.isBlank(pluginId)) {
+//            throw exception(PLUGIN_INFO_NOT_EXISTS);
+//        }
+//        com.gitee.starblues.core.PluginInfo pluginInfo = pluginOperator.getPluginInfo(pluginId);
+//        if (pluginInfo != null) {
+//            if (pluginInfoDo.getStatus().equals(IotPluginStatusEnum.RUNNING.getStatus()) && pluginInfo.getPluginState() != PluginState.STARTED) {
+//                // 启动插件
+//                pluginOperator.start(pluginId);
+//            } else if (pluginInfoDo.getStatus().equals(IotPluginStatusEnum.STOPPED.getStatus()) && pluginInfo.getPluginState() == PluginState.STARTED) {
+//                // 停止插件
+//                pluginOperator.stop(pluginId);
+//            }
+//        } else {
+//            // 已经停止,未获取到插件
+//            if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
+//                throw exception(PLUGIN_STATUS_INVALID);
+//            }
+//        }
+        pluginInfoDo.setStatus(status);
+        pluginInfoMapper.updateById(pluginInfoDo);
+    }
+
+    @PostConstruct
+    public void init() {
+        Executors.newSingleThreadScheduledExecutor().schedule(this::startPlugins, 3, TimeUnit.SECONDS);
+    }
+
+    @SneakyThrows
+    private void startPlugins() {
+//        while (!pluginOperator.inited()) {
+//            Thread.sleep(1000L);
+//        }
+
+        for (PluginInfoDO pluginInfoDO : pluginInfoMapper.selectList()) {
+            if (!IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
+                continue;
+            }
+            log.info("start plugin:{}", pluginInfoDO.getPluginId());
+            try {
+//                com.gitee.starblues.core.PluginInfo plugin = pluginOperator.getPluginInfo(pluginInfoDO.getPluginId());
+//                if (plugin != null) {
+//                    pluginOperator.start(plugin.getPluginId());
+//                }
+            } catch (Exception e) {
+                log.error("start plugin error", e);
+            }
+        }
+    }
+
+}

+ 54 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininstance/PluginInstanceService.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.iot.service.plugininstance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstancePageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstanceSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
+import jakarta.validation.Valid;
+
+/**
+ * IoT 插件实例 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PluginInstanceService {
+
+    /**
+     * 创建IoT 插件实例
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createPluginInstance(@Valid PluginInstanceSaveReqVO createReqVO);
+
+    /**
+     * 更新IoT 插件实例
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePluginInstance(@Valid PluginInstanceSaveReqVO updateReqVO);
+
+    /**
+     * 删除IoT 插件实例
+     *
+     * @param id 编号
+     */
+    void deletePluginInstance(Long id);
+
+    /**
+     * 获得IoT 插件实例
+     *
+     * @param id 编号
+     * @return IoT 插件实例
+     */
+    PluginInstanceDO getPluginInstance(Long id);
+
+    /**
+     * 获得IoT 插件实例分页
+     *
+     * @param pageReqVO 分页查询
+     * @return IoT 插件实例分页
+     */
+    PageResult<PluginInstanceDO> getPluginInstancePage(PluginInstancePageReqVO pageReqVO);
+
+}

+ 70 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininstance/PluginInstanceServiceImpl.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.iot.service.plugininstance;
+
+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.plugininstance.vo.PluginInstancePageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstanceSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.plugininstance.PluginInstanceMapper;
+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.PLUGIN_INSTANCE_NOT_EXISTS;
+
+/**
+ * IoT 插件实例 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class PluginInstanceServiceImpl implements PluginInstanceService {
+
+    @Resource
+    private PluginInstanceMapper pluginInstanceMapper;
+
+    @Override
+    public Long createPluginInstance(PluginInstanceSaveReqVO createReqVO) {
+        // 插入
+        PluginInstanceDO pluginInstance = BeanUtils.toBean(createReqVO, PluginInstanceDO.class);
+        pluginInstanceMapper.insert(pluginInstance);
+        // 返回
+        return pluginInstance.getId();
+    }
+
+    @Override
+    public void updatePluginInstance(PluginInstanceSaveReqVO updateReqVO) {
+        // 校验存在
+        validatePluginInstanceExists(updateReqVO.getId());
+        // 更新
+        PluginInstanceDO updateObj = BeanUtils.toBean(updateReqVO, PluginInstanceDO.class);
+        pluginInstanceMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deletePluginInstance(Long id) {
+        // 校验存在
+        validatePluginInstanceExists(id);
+        // 删除
+        pluginInstanceMapper.deleteById(id);
+    }
+
+    private void validatePluginInstanceExists(Long id) {
+        if (pluginInstanceMapper.selectById(id) == null) {
+            throw exception(PLUGIN_INSTANCE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public PluginInstanceDO getPluginInstance(Long id) {
+        return pluginInstanceMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<PluginInstanceDO> getPluginInstancePage(PluginInstancePageReqVO pageReqVO) {
+        return pluginInstanceMapper.selectPage(pageReqVO);
+    }
+
+}

+ 16 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java

@@ -45,6 +45,22 @@ public interface IotProductService {
      */
     IotProductDO getProduct(Long id);
 
+    /**
+     * 校验产品存在
+     *
+     * @param id 编号
+     * @return 产品
+     */
+    IotProductDO validateProductExists(Long id);
+
+    /**
+     * 校验产品存在
+     *
+     * @param productKey 产品 key
+     * @return 产品
+     */
+    IotProductDO validateProductExists(String productKey);
+
     /**
      * 获得产品分页
      *

+ 16 - 6
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java

@@ -71,16 +71,26 @@ public class IotProductServiceImpl implements IotProductService {
         productMapper.deleteById(id);
     }
 
-    private IotProductDO validateProductExists(Long id) {
-        IotProductDO iotProductDO = productMapper.selectById(id);
-        if (iotProductDO == null) {
+    @Override
+    public IotProductDO validateProductExists(Long id) {
+        IotProductDO product = productMapper.selectById(id);
+        if (product == null) {
+            throw exception(PRODUCT_NOT_EXISTS);
+        }
+        return product;
+    }
+
+    @Override
+    public IotProductDO validateProductExists(String productKey) {
+        IotProductDO product = productMapper.selectByProductKey(productKey);
+        if (product == null) {
             throw exception(PRODUCT_NOT_EXISTS);
         }
-        return iotProductDO;
+        return product;
     }
 
-    private void validateProductStatus(IotProductDO iotProductDO) {
-        if (Objects.equals(iotProductDO.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) {
+    private void validateProductStatus(IotProductDO product) {
+        if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) {
             throw exception(PRODUCT_STATUS_NOT_DELETE);
         }
     }

+ 12 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/plugininfo/PluginInfoMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 12 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/plugininstance/PluginInstanceMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.iot.dal.mysql.plugininstance.PluginInstanceMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 31 - 0
yudao-module-iot/yudao-module-iot-plugin/pom.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-module-iot</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-iot-plugin</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        物联网 模块 - 插件
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.pf4j</groupId>
+            <artifactId>pf4j-spring</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 6 - 0
yudao-module-iot/yudao-module-iot-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 占位
+ *
+ * TODO 芋艿:后续删除
+ */
+package cn.iocoder.yudao.module.iot.plugin;