浏览代码

Merge branch 'feature/iot' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into origin/feature/iot

# Conflicts:
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
alwayssuper 5 月之前
父节点
当前提交
9f8c6a944c
共有 74 个文件被更改,包括 3398 次插入361 次删除
  1. 1 1
      yudao-dependencies/pom.xml
  2. 1 0
      yudao-framework/yudao-spring-boot-starter-mq/pom.xml
  3. 9 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java
  4. 33 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java
  5. 15 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
  6. 4 3
      yudao-module-iot/yudao-module-iot-biz/pom.xml
  7. 8 3
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java
  8. 9 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
  9. 25 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java
  10. 0 12
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
  11. 62 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java
  12. 77 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java
  13. 65 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java
  14. 27 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCommonReqVO.java
  15. 79 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java
  16. 23 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java
  17. 85 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java
  18. 20 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java
  19. 57 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java
  20. 109 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java
  21. 27 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java
  22. 84 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java
  23. 69 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java
  24. 35 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/ota/IotOtaUpgradeRecordConvert.java
  25. 18 5
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java
  26. 5 2
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
  27. 45 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java
  28. 135 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java
  29. 58 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java
  30. 35 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeRecordJob.java
  31. 72 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeTaskJob.java
  32. 13 4
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
  33. 26 36
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
  34. 7 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java
  35. 45 11
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java
  36. 58 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java
  37. 93 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java
  38. 114 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java
  39. 212 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java
  40. 68 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java
  41. 249 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java
  42. 79 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/bo/IotOtaUpgradeRecordCreateReqBO.java
  43. 46 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/bo/IotOtaUpgradeRecordUpdateReqBO.java
  44. 6 154
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java
  45. 32 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java
  46. 93 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java
  47. 131 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java
  48. 67 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java
  49. 24 0
      yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/ota/IotOtaUpgradeRecordMapper.xml
  50. 2 2
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java
  51. 11 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java
  52. 1 1
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java
  53. 6 1
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java
  54. 1 1
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java
  55. 1 1
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java
  56. 3 2
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java
  57. 3 2
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java
  58. 6 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java
  59. 8 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java
  60. 4 4
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties
  61. 59 55
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml
  62. 0 42
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/EmqxPlugin.java
  63. 22 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java
  64. 57 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java
  65. 31 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java
  66. 49 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java
  67. 46 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java
  68. 164 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java
  69. 54 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java
  70. 194 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java
  71. 19 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml
  72. 1 1
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceEventReportVertxHandler.java
  73. 1 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml
  74. 0 17
      yudao-server/src/main/resources/application-local.yaml

+ 1 - 1
yudao-dependencies/pom.xml

@@ -67,7 +67,7 @@
         <netty.version>4.1.116.Final</netty.version>
         <mqtt.version>1.2.5</mqtt.version>
         <pf4j-spring.version>0.9.0</pf4j-spring.version>
-        <vertx.version>4.5.11</vertx.version>
+        <vertx.version>4.5.13</vertx.version>
         <!-- 三方云服务相关 -->
         <commons-io.version>2.17.0</commons-io.version>
         <commons-compress.version>1.27.1</commons-compress.version>

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-mq/pom.xml

@@ -36,6 +36,7 @@
         <dependency>
             <groupId>org.apache.rocketmq</groupId>
             <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
         </dependency>
     </dependencies>
 

+ 9 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java

@@ -71,6 +71,15 @@ public interface IotDeviceUpstreamApi {
     @PostMapping(PREFIX + "/add-topology")
     CommonResult<Boolean> addDeviceTopology(@Valid @RequestBody IotDeviceTopologyAddReqDTO addReqDTO);
 
+    // TODO @芋艿:考虑 http 认证
+    /**
+     * 认证 Emqx 连接
+     *
+     * @param authReqDTO 认证 Emqx 连接 DTO
+     */
+    @PostMapping(PREFIX + "/authenticate-emqx-connection")
+    CommonResult<Boolean> authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO);
+
     // ========== 插件相关 ==========
 
     /**

+ 33 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO
+/**
+ * IoT 认证 Emqx 连接 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class IotDeviceEmqxAuthReqDTO {
+
+    /**
+     * 客户端 ID
+     */
+    @NotEmpty(message = "客户端 ID 不能为空")
+    private String clientId;
+
+    /**
+     * 用户名
+     */
+    @NotEmpty(message = "用户名不能为空")
+    private String username;
+
+    /**
+     * 密码
+     */
+    @NotEmpty(message = "密码不能为空")
+    private String password;
+
+}

+ 15 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java

@@ -51,5 +51,20 @@ public interface ErrorCodeConstants {
 
     // ========== 插件实例 1-050-007-000 ==========
 
+    // ========== 固件相关 1-050-008-000 ==========
+
+    ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在");
+    ErrorCode OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE = new ErrorCode(1_050_008_001, "产品版本号重复");
+
+    // TODO @li:1_050_008_100,这样有点间隔?
+    ErrorCode OTA_UPGRADE_TASK_NOT_EXISTS = new ErrorCode(1_050_008_002, "升级任务不存在");
+    ErrorCode OTA_UPGRADE_TASK_NAME_DUPLICATE = new ErrorCode(1_050_008_003, "升级任务名称重复");
+    ErrorCode OTA_UPGRADE_TASK_PARAMS_INVALID = new ErrorCode(1_050_008_004, "升级任务参数无效");
+    ErrorCode OTA_UPGRADE_TASK_CANNOT_CANCEL = new ErrorCode(1_050_008_005, "升级任务不能取消");
+
+    // TODO @li:1_050_008_200
+    ErrorCode OTA_UPGRADE_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_006, "升级记录不存在");
+    ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_007, "升级记录重复");
+    ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_008, "升级记录不能重试");
 
 }

+ 4 - 3
yudao-module-iot/yudao-module-iot-biz/pom.xml

@@ -75,10 +75,11 @@
 <!--            <groupId>org.eclipse.paho</groupId> &lt;!&ndash; MQTT &ndash;&gt;-->
 <!--            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>-->
 <!--        </dependency>-->
-        <!-- 工具类相关 -->
+        <!-- 消息队列相关 -->
         <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-mq</artifactId>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>

+ 8 - 3
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java

@@ -4,15 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService;
 import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
+import jakarta.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.RestController;
 
-import jakarta.annotation.Resource;
-
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 /**
- *  * 设备数据 Upstream 上行 API 实现类
+ * * 设备数据 Upstream 上行 API 实现类
  */
 @RestController
 @Validated
@@ -61,6 +60,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
         return success(true);
     }
 
+    @Override
+    public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
+        boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO);
+        return success(result);
+    }
+
     // ========== 插件相关 ==========
 
     @Override

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

@@ -7,8 +7,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO;
-import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService;
@@ -177,4 +177,12 @@ public class IotDeviceController {
         return success(true);
     }
 
+    // TODO @haohao:是不是默认详情接口,不返回 secret,然后这个接口,用于统一返回。然后接口名可以更通用一点。
+    @GetMapping("/mqtt-connection-params")
+    @Operation(summary = "获取 MQTT 连接参数")
+    @PreAuthorize("@ss.hasPermission('iot:device:mqtt-connection-params')")
+    public CommonResult<IotDeviceMqttConnectionParamsRespVO> getMqttConnectionParams(@RequestParam("deviceId") Long deviceId) {
+        return success(deviceService.getMqttConnectionParams(deviceId));
+    }
+
 }

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

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IoT 设备 MQTT 连接参数 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class IotDeviceMqttConnectionParamsRespVO {
+
+    @Schema(description = "MQTT 客户端 ID", example = "24602")
+    @ExcelProperty("MQTT 客户端 ID")
+    private String mqttClientId;
+
+    @Schema(description = "MQTT 用户名", example = "芋艿")
+    @ExcelProperty("MQTT 用户名")
+    private String mqttUsername;
+
+    @Schema(description = "MQTT 密码")
+    @ExcelProperty("MQTT 密码")
+    private String mqttPassword;
+
+}

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

@@ -79,18 +79,6 @@ public class IotDeviceRespVO {
     @ExcelProperty("设备密钥")
     private String deviceSecret;
 
-    @Schema(description = "MQTT 客户端 ID", example = "24602")
-    @ExcelProperty("MQTT 客户端 ID")
-    private String mqttClientId;
-
-    @Schema(description = "MQTT 用户名", example = "芋艿")
-    @ExcelProperty("MQTT 用户名")
-    private String mqttUsername;
-
-    @Schema(description = "MQTT 密码")
-    @ExcelProperty("MQTT 密码")
-    private String mqttPassword;
-
     @Schema(description = "认证类型(如一机一密、动态注册)", example = "2")
     @ExcelProperty("认证类型(如一机一密、动态注册)")
     private String authType;

+ 62 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareRespVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Validated
+@RestController
+@Tag(name = "管理后台 - IoT OTA 固件")
+@RequestMapping("/iot/ota-firmware")
+public class IotOtaFirmwareController {
+
+    @Resource
+    private IotOtaFirmwareService otaFirmwareService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建 OTA 固件")
+    @PreAuthorize("@ss.hasPermission('iot:ota-firmware:create')")
+    public CommonResult<Long> createOtaFirmware(@Valid @RequestBody IotOtaFirmwareCreateReqVO createReqVO) {
+        return success(otaFirmwareService.createOtaFirmware(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新 OTA 固件")
+    @PreAuthorize("@ss.hasPermission('iot:ota-firmware:update')")
+    public CommonResult<Boolean> updateOtaFirmware(@Valid @RequestBody IotOtaFirmwareUpdateReqVO updateReqVO) {
+        otaFirmwareService.updateOtaFirmware(updateReqVO);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得 OTA 固件")
+    @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')")
+    public CommonResult<IotOtaFirmwareRespVO> getOtaFirmware(@RequestParam("id") Long id) {
+        IotOtaFirmwareDO otaFirmware = otaFirmwareService.getOtaFirmware(id);
+        return success(BeanUtils.toBean(otaFirmware, IotOtaFirmwareRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得 OTA 固件分页")
+    @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')")
+    public CommonResult<PageResult<IotOtaFirmwareRespVO>> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO) {
+        PageResult<IotOtaFirmwareDO> pageResult = otaFirmwareService.getOtaFirmwarePage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, IotOtaFirmwareRespVO.class));
+    }
+
+}

+ 77 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordRespVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Validated
+@RestController
+@Tag(name = "管理后台 - OTA 升级记录")
+@RequestMapping("/iot/ota-upgrade-record")
+public class IotOtaUpgradeRecordController {
+
+    @Resource
+    private IotOtaUpgradeRecordService upgradeRecordService;
+
+    @GetMapping("/get-statistics")
+    @Operation(summary = "固件升级设备统计")
+    @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')")
+    @Parameter(name = "firmwareId", description = "固件编号", required = true, example = "1024")
+    public CommonResult<Map<Integer, Long>> getOtaUpgradeRecordStatistics(
+            @RequestParam(value = "firmwareId") Long firmwareId) {
+        return success(upgradeRecordService.getOtaUpgradeRecordStatistics(firmwareId));
+    }
+
+    @GetMapping("/get-count")
+    @Operation(summary = "获得升级记录分页 tab 数量")
+    @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')")
+    public CommonResult<Map<Integer, Long>> getOtaUpgradeRecordCount(
+            @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) {
+        return success(upgradeRecordService.getOtaUpgradeRecordCount(pageReqVO));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得升级记录分页")
+    @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')")
+    public CommonResult<PageResult<IotOtaUpgradeRecordRespVO>> getUpgradeRecordPage(
+            @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) {
+        PageResult<IotOtaUpgradeRecordDO> pageResult = upgradeRecordService.getUpgradeRecordPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, IotOtaUpgradeRecordRespVO.class));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得升级记录")
+    @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')")
+    @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024")
+    public CommonResult<IotOtaUpgradeRecordRespVO> getUpgradeRecord(@RequestParam("id") Long id) {
+        IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordService.getUpgradeRecord(id);
+        return success(BeanUtils.toBean(upgradeRecord, IotOtaUpgradeRecordRespVO.class));
+    }
+
+    // TODO @li:使用 Putmapping
+    @PostMapping("/retry")
+    @Operation(summary = "重试升级记录")
+    @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:retry')")
+    @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024")
+    public CommonResult<Boolean> retryUpgradeRecord(@RequestParam("id") Long id) {
+        upgradeRecordService.retryUpgradeRecord(id);
+        return success(true);
+    }
+
+}

+ 65 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java

@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskRespVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Validated
+@RestController
+@Tag(name = "管理后台 - OTA升级任务")
+@RequestMapping("/iot/ota-upgrade-task")
+public class IotOtaUpgradeTaskController {
+
+    @Resource
+    private IotOtaUpgradeTaskService upgradeTaskService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建升级任务")
+    @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:create')")
+    public CommonResult<Long> createUpgradeTask(@Valid @RequestBody IotOtaUpgradeTaskSaveReqVO createReqVO) {
+        return success(upgradeTaskService.createUpgradeTask(createReqVO));
+    }
+
+    @PostMapping("/cancel")
+    @Operation(summary = "取消升级任务")
+    @Parameter(name = "id", description = "升级任务编号", required = true)
+    @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:cancel')")
+    public CommonResult<Boolean> cancelUpgradeTask(@RequestParam("id") Long id) {
+        upgradeTaskService.cancelUpgradeTask(id);
+        return success(true);
+    }
+
+    // TODO @li:get 接口,不是 @RequestBody 哈
+    @GetMapping("/page")
+    @Operation(summary = "获得升级任务分页")
+    @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')")
+    public CommonResult<PageResult<IotOtaUpgradeTaskRespVO>> getUpgradeTaskPage(@Valid @RequestBody IotOtaUpgradeTaskPageReqVO pageReqVO) {
+        PageResult<IotOtaUpgradeTaskDO> pageResult = upgradeTaskService.getUpgradeTaskPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, IotOtaUpgradeTaskRespVO.class));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得升级任务")
+    @Parameter(name = "id", description = "升级任务编号", required = true, example = "1024")
+    @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')")
+    public CommonResult<IotOtaUpgradeTaskRespVO> getUpgradeTask(@RequestParam("id") Long id) {
+        IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(id);
+        return success(BeanUtils.toBean(upgradeTask, IotOtaUpgradeTaskRespVO.class));
+    }
+
+}

+ 27 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCommonReqVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+// TODO @li:因为 create 和 update 可以公用的字段比较少,建议不用 IotOtaFirmwareCommonReqVO
+@Data
+@Schema(description = "管理后台 - OTA固件信息 Request VO")
+public class IotOtaFirmwareCommonReqVO {
+
+    /**
+     * 固件名称
+     */
+    @NotEmpty(message = "固件名称不能为空")
+    @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件")
+    private String name;
+
+    /**
+     * 固件描述
+     */
+    @Schema(description = "固件描述", example = "某品牌型号固件,测试用")
+    private String description;
+
+}

+ 79 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java

@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+// TODO @li:中英文之间,有个空格。中文写作习惯哈。
+@Schema(description = "管理后台 - OTA固件创建 Request VO")
+@Data
+public class IotOtaFirmwareCreateReqVO extends IotOtaFirmwareCommonReqVO {
+
+    // TODO @li:因为有了注解,注释可以不写哈
+    // TODO @li:swagger 注解,写在 validator 注解之前,保持项目统一哈。
+    /**
+     * 版本号
+     */
+    @NotEmpty(message = "版本号不能为空")
+    @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
+    private String version;
+
+    /**
+     * 产品编号
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
+     */
+    @NotNull(message = "产品编号不能为空")
+    @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
+    private String productId;
+
+    // TODO @li:productId 即可,而 productKey 通过 productId 查询
+    /**
+     * 产品标识
+     * <p>
+     * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
+     */
+    @NotEmpty(message = "产品标识不能为空")
+    @Schema(description = "产品标识", requiredMode = REQUIRED, example = "yudao")
+    private String productKey;
+
+    /**
+     * 签名方式
+     * <p>
+     * 例如说:MD5、SHA256
+     */
+    @Schema(description = "签名方式", example = "MD5")
+    private String signMethod;
+
+    // TODO @li:fileSign、fileSize 通过后端下载文件,计算出来。对前端屏蔽这个细节。
+
+    /**
+     * 固件文件签名
+     */
+    @Schema(description = "固件文件签名", example = "d41d8cd98f00b204e9800998ecf8427e")
+    private String fileSign;
+
+    /**
+     * 固件文件大小
+     */
+    @NotNull(message = "固件文件大小不能为空")
+    @Schema(description = "固件文件大小(单位:byte)", example = "1024")
+    private Long fileSize;
+
+    /**
+     * 固件文件 URL
+     */
+    @NotEmpty(message = "固件文件 URL 不能为空")
+    @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
+    private String fileUrl;
+
+    /**
+     * 自定义信息,建议使用 JSON 格式
+     */
+    @Schema(description = "自定义信息,建议使用 JSON 格式", example = "{\"key1\":\"value1\",\"key2\":\"value2\"}")
+    private String information;
+
+}

+ 23 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "管理后台 - OTA 固件分页 Request VO")
+public class IotOtaFirmwarePageReqVO extends PageParam {
+
+    /**
+     * 固件名称
+     */
+    @Schema(description = "固件名称", example = "智能开关固件")
+    private String name;
+
+    /**
+     * 产品标识
+     */
+    @Schema(description = "产品标识", example = "1024")
+    private String productId;
+
+}

+ 85 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java

@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
+import com.fhs.core.trans.anno.Trans;
+import com.fhs.core.trans.constant.TransType;
+import com.fhs.core.trans.vo.VO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA 固件 Response VO")
+public class IotOtaFirmwareRespVO implements VO {
+
+    /**
+     * 固件编号
+     */
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    private Long id;
+    /**
+     * 固件名称
+     */
+    @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件")
+    private String name;
+    /**
+     * 固件描述
+     */
+    @Schema(description = "固件描述")
+    private String description;
+    /**
+     * 版本号
+     */
+    @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
+    private String version;
+
+    /**
+     * 产品编号
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
+     */
+    @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
+    @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"})
+    private String productId;
+    /**
+     * 产品标识
+     * <p>
+     * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
+     */
+    @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key")
+    private String productKey;
+    /**
+     * 产品名称
+     */
+    @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品")
+    private String productName;
+    /**
+     * 签名方式
+     * <p>
+     * 例如说:MD5、SHA256
+     */
+    @Schema(description = "签名方式", example = "MD5")
+    private String signMethod;
+    /**
+     * 固件文件签名
+     */
+    @Schema(description = "固件文件签名", example = "1024")
+    private String fileSign;
+    /**
+     * 固件文件大小
+     */
+    @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024")
+    private Long fileSize;
+    /**
+     * 固件文件 URL
+     */
+    @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn")
+    private String fileUrl;
+    /**
+     * 自定义信息,建议使用 JSON 格式
+     */
+    @Schema(description = "自定义信息,建议使用 JSON 格式")
+    private String information;
+
+}

+ 20 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA固件更新 Request VO")
+public class IotOtaFirmwareUpdateReqVO extends IotOtaFirmwareCommonReqVO {
+
+    /**
+     * 固件编号
+     */
+    @NotNull(message = "固件编号不能为空")
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    private Long id;
+
+}

+ 57 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA升级记录分页 Request VO")
+public class IotOtaUpgradeRecordPageReqVO extends PageParam {
+
+    // TODO @li:使用 IotOtaUpgradeRecordStatusEnum 枚举哈
+    /**
+     * 待处理状态
+     */
+    public static final Integer PENDING = 0;
+    /**
+     * 已推送状态
+     */
+    public static final Integer PUSHED = 10;
+    /**
+     * 正在升级状态
+     */
+    public static final Integer UPGRADING = 20;
+    /**
+     * 升级成功状态
+     */
+    public static final Integer SUCCESS = 30;
+    /**
+     * 升级失败状态
+     */
+    public static final Integer FAILURE = 40;
+    /**
+     * 升级已取消状态
+     */
+    public static final Integer CANCELED = 50;
+
+    /**
+     * 升级任务编号字段。
+     * <p>
+     * 该字段用于标识升级任务的唯一编号,不能为空。
+     */
+    @NotNull(message = "升级任务编号不能为空")
+    @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024")
+    private Long taskId;
+
+    /**
+     * 设备标识字段。
+     * <p>
+     * 该字段用于标识设备的名称,通常用于区分不同的设备。
+     */
+    @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1")
+    private String deviceName;
+
+}

+ 109 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import com.fhs.core.trans.anno.Trans;
+import com.fhs.core.trans.constant.TransType;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA升级记录 Response VO")
+public class IotOtaUpgradeRecordRespVO {
+
+    /**
+     * 升级记录编号
+     */
+    @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024")
+    private Long id;
+    /**
+     * 固件编号
+     * <p>
+     * 关联 {@link IotOtaFirmwareDO#getId()}
+     */
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"})
+    private Long firmwareId;
+    /**
+     * 固件版本
+     */
+    @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0")
+    private String firmwareVersion;
+    /**
+     * 任务编号
+     * <p>
+     * 关联 {@link IotOtaUpgradeTaskDO#getId()}
+     */
+    @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
+    private Long taskId;
+    /**
+     * 产品标识
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
+     */
+    @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot")
+    private String productKey;
+    /**
+     * 设备名称
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
+     */
+    @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot")
+    private String deviceName;
+    /**
+     * 设备编号
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
+     */
+    @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024")
+    private String deviceId;
+    /**
+     * 来源的固件编号
+     * <p>
+     * 关联 {@link IotDeviceDO#getFirmwareId()}
+     */
+    @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024")
+    @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"})
+    private Long fromFirmwareId;
+    /**
+     * 来源的固件版本
+     */
+    @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0")
+    private String fromFirmwareVersion;
+    /**
+     * 升级状态
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
+     */
+    @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
+    private Integer status;
+    /**
+     * 升级进度,百分比
+     */
+    @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10")
+    private Integer progress;
+    /**
+     * 升级进度描述
+     * <p>
+     * 注意,只记录设备最后一次的升级进度描述
+     * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志
+     */
+    @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10")
+    private String description;
+    /**
+     * 升级开始时间
+     */
+    @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    private LocalDateTime startTime;
+    /**
+     * 升级结束时间
+     */
+    @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    private LocalDateTime endTime;
+
+}

+ 27 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA升级任务分页 Request VO")
+public class IotOtaUpgradeTaskPageReqVO extends PageParam {
+
+    /**
+     * 任务名称字段,用于描述任务的名称
+     */
+    @Schema(description = "任务名称", example = "升级任务")
+    private String name;
+
+    /**
+     * 固件编号字段,用于唯一标识固件,不能为空
+     */
+    @NotNull(message = "固件编号不能为空")
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    private Long firmwareId;
+
+}

+ 84 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import com.fhs.core.trans.vo.VO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA升级任务 Response VO")
+public class IotOtaUpgradeTaskRespVO implements VO {
+
+    /**
+     * 任务编号
+     */
+    @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
+    private Long id;
+    /**
+     * 任务名称
+     */
+    @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
+    private String name;
+    /**
+     * 任务描述
+     */
+    @Schema(description = "任务描述", example = "升级任务")
+    private String description;
+    /**
+     * 固件编号
+     * <p>
+     * 关联 {@link IotOtaFirmwareDO#getId()}
+     */
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    private Long firmwareId;
+    /**
+     * 任务状态
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum}
+     */
+    @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"})
+    private Integer status;
+    /**
+     * 任务状态名称
+     */
+    @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中")
+    private String statusName;
+    /**
+     * 升级范围
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
+     */
+    @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"})
+    private Integer scope;
+    /**
+     * 设备数量
+     */
+    @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024")
+    private Long deviceCount;
+    /**
+     * 选中的设备编号数组
+     * <p>
+     * 关联 {@link IotDeviceDO#getId()}
+     */
+    @Schema(description = "选中的设备编号数组", example = "1024")
+    private List<Long> deviceIds;
+    /**
+     * 选中的设备名字数组
+     * <p>
+     * 关联 {@link IotDeviceDO#getDeviceName()}
+     */
+    @Schema(description = "选中的设备名字数组", example = "1024")
+    private List<String> deviceNames;
+    /**
+     * 创建时间
+     */
+    @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    private LocalDateTime createTime;
+
+}

+ 69 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java

@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Data
+@Schema(description = "管理后台 - OTA升级任务创建/修改 Request VO")
+public class IotOtaUpgradeTaskSaveReqVO {
+
+    /**
+     * 任务名称
+     */
+    @NotEmpty(message = "任务名称不能为空")
+    @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
+    private String name;
+
+    /**
+     * 任务描述
+     */
+    @Schema(description = "任务描述", example = "升级任务")
+    private String description;
+
+    /**
+     * 固件编号
+     * <p>
+     * 关联 {@link IotOtaFirmwareDO#getId()}
+     */
+    @NotNull(message = "固件编号不能为空")
+    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    private Long firmwareId;
+
+    /**
+     * 升级范围
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
+     */
+    @NotNull(message = "升级范围不能为空")
+    @InEnum(value = IotOtaUpgradeTaskScopeEnum.class)
+    @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1")
+    private Integer scope;
+
+    /**
+     * 选中的设备编号数组
+     * <p>
+     * 关联 {@link IotDeviceDO#getId()}
+     */
+    @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]")
+    private List<Long> deviceIds;
+
+    // TODO @li:通过 deviceIds 查询 deviceNames,前端不传递哈
+    /**
+     * 选中的设备名字数组
+     * <p>
+     * 关联 {@link IotDeviceDO#getDeviceName()}
+     */
+    @Schema(description = "选中的设备名字数组", requiredMode = REQUIRED, example = "[设备1,设备2,设备3,设备4]")
+    private List<String> deviceNames;
+
+}

+ 35 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/ota/IotOtaUpgradeRecordConvert.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.convert.ota;
+
+import cn.hutool.core.convert.Convert;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordCreateReqBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface IotOtaUpgradeRecordConvert {
+
+    IotOtaUpgradeRecordConvert INSTANCE = Mappers.getMapper(IotOtaUpgradeRecordConvert.class);
+
+    // TODO @li:一般情况下,这种 convert 直接写 service 就好啦。不用特别写一个哈
+    default List<IotOtaUpgradeRecordCreateReqBO> convertBOList(IotOtaUpgradeTaskDO upgradeTask, IotOtaFirmwareDO firmware, List<IotDeviceDO> deviceList) {
+        return deviceList.stream().map(device -> {
+            IotOtaUpgradeRecordCreateReqBO createReqBO = new IotOtaUpgradeRecordCreateReqBO();
+            createReqBO.setFirmwareId(firmware.getId());
+            createReqBO.setTaskId(upgradeTask.getId());
+            createReqBO.setProductKey(device.getProductKey());
+            createReqBO.setDeviceName(device.getDeviceName());
+            createReqBO.setDeviceId(Convert.toStr(device.getId()));
+            createReqBO.setFromFirmwareId(Convert.toLong(device.getFirmwareId()));
+            createReqBO.setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus());
+            createReqBO.setProgress(0);
+            return createReqBO;
+        }).toList();
+    }
+
+}

+ 18 - 5
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java

@@ -41,30 +41,43 @@ public class IotOtaUpgradeTaskDO extends BaseDO {
 
     /**
      * 固件编号
-     *
+     * <p>
      * 关联 {@link IotOtaFirmwareDO#getId()}
      */
     private Long firmwareId;
 
     /**
      * 任务状态
-     *
+     * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum}
      */
     private Integer status;
 
     /**
      * 升级范围
-     *
+     * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
      */
     private Integer scope;
+    /**
+     * 设备数量
+     */
+    private Long deviceCount;
+    /**
+     * 选中的设备编号数组
+     * <p>
+     * 关联 {@link IotDeviceDO#getId()}
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<Long> deviceIds;
+
+    // TODO @li:这个通过查询,不用冗余
     /**
      * 选中的设备名字数组
-     *
+     * <p>
      * 关联 {@link IotDeviceDO#getDeviceName()}
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> deviceNames;
 
-}
+}

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

@@ -61,10 +61,13 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
         return selectList(IotDeviceDO::getState, state);
     }
 
+    default List<IotDeviceDO> selectListByProductId(Long productId) {
+        return selectList(IotDeviceDO::getProductId, productId);
+    }
+
     default Long selectCountByGroupId(Long groupId) {
         return selectCount(new LambdaQueryWrapperX<IotDeviceDO>()
-                .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0")
-                .orderByDesc(IotDeviceDO::getId));
+                .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0"));
     }
 
     /**

+ 45 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+// TODO @li:这里的注释,可以去掉哈,多了点点
+/**
+ * IotOtaFirmwareMapper 接口用于操作 IotOtaFirmwareDO 实体类对应的数据库表。
+ * 该接口继承自 BaseMapperX,提供了基本的 CRUD 操作,并扩展了特定查询方法。
+ */
+@Mapper
+public interface IotOtaFirmwareMapper extends BaseMapperX<IotOtaFirmwareDO> {
+
+    /**
+     * 根据产品ID和固件版本号查询固件信息列表。
+     *
+     * @param productId 产品ID,用于筛选固件信息。
+     * @param version   固件版本号,用于筛选固件信息。
+     * @return 返回符合条件的固件信息列表。
+     */
+    default List<IotOtaFirmwareDO> selectByProductIdAndVersion(String productId, String version) {
+        return selectList(IotOtaFirmwareDO::getProductId, productId,
+                IotOtaFirmwareDO::getVersion, version);
+    }
+
+    /**
+     * 分页查询固件信息,支持根据名称和产品ID进行筛选,并按创建时间降序排列。
+     *
+     * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件。
+     * @return 返回分页查询结果,包含符合条件的固件信息列表。
+     */
+    default PageResult<IotOtaFirmwareDO> selectPage(IotOtaFirmwarePageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<IotOtaFirmwareDO>()
+                .likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName())
+                .eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId())
+                .orderByDesc(IotOtaFirmwareDO::getCreateTime));
+    }
+
+}

+ 135 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java

@@ -0,0 +1,135 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+// TODO @li:这里的注释,可以去掉哈,多了点点
+/**
+ * OTA 升级记录 Mapper 接口
+ */
+@Mapper
+public interface IotOtaUpgradeRecordMapper extends BaseMapperX<IotOtaUpgradeRecordDO> {
+
+    /**
+     * 根据条件查询单个OTA升级记录
+     *
+     * @param firmwareId 固件ID,可选参数,用于筛选固件ID匹配的记录
+     * @param taskId     任务ID,可选参数,用于筛选任务ID匹配的记录
+     * @param deviceId   设备ID,可选参数,用于筛选设备ID匹配的记录
+     * @return 返回符合条件的单个OTA升级记录,如果不存在则返回null
+     */
+    default IotOtaUpgradeRecordDO selectByConditions(Long firmwareId, Long taskId, String deviceId) {
+        // 使用LambdaQueryWrapperX构建查询条件,根据传入的参数动态添加查询条件
+        return selectOne(new LambdaQueryWrapperX<IotOtaUpgradeRecordDO>()
+                .eqIfPresent(IotOtaUpgradeRecordDO::getFirmwareId, firmwareId)
+                .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, taskId)
+                .eqIfPresent(IotOtaUpgradeRecordDO::getDeviceId, deviceId));
+    }
+
+    /**
+     * 获取OTA升级记录的数量
+     *
+     * @param taskId     任务ID,用于筛选特定任务的升级记录
+     * @param deviceName 设备名称,用于筛选特定设备的升级记录
+     * @param status     状态,用于筛选特定状态的升级记录
+     * @return 返回符合条件的OTA升级记录的数量
+     */
+    Long getOtaUpgradeRecordCount(@Param("taskId") Long taskId,
+                                  @Param("deviceName") String deviceName,
+                                  @Param("status") Integer status);
+
+    /**
+     * 获取OTA升级记录的统计信息
+     *
+     * @param firmwareId 固件ID,用于筛选特定固件的升级记录
+     * @param status     状态,用于筛选特定状态的升级记录
+     * @return 返回符合条件的OTA升级记录的统计信息
+     */
+    Long getOtaUpgradeRecordStatistics(@Param("firmwareId") Long firmwareId,
+                                       @Param("status") Integer status);
+
+
+    /**
+     * 根据分页查询条件获取IOT OTA升级记录的分页结果
+     *
+     * @param pageReqVO 分页查询请求参数,包含设备名称、任务ID等查询条件
+     * @return 返回分页查询结果,包含符合条件的IOT OTA升级记录列表
+     */
+    default PageResult<IotOtaUpgradeRecordDO> selectUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) {
+        // 使用LambdaQueryWrapperX构建查询条件,并根据请求参数动态添加查询条件
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<IotOtaUpgradeRecordDO>()
+                .likeIfPresent(IotOtaUpgradeRecordDO::getDeviceName, pageReqVO.getDeviceName()) // 如果设备名称存在,则添加模糊查询条件
+                .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, pageReqVO.getTaskId())); // 如果任务ID存在,则添加等值查询条件
+    }
+
+    /**
+     * 根据任务ID取消升级记录。
+     * 该方法通过任务ID查找状态为“待处理”的升级记录,并将其状态更新为“已取消”。
+     *
+     * @param taskId 任务ID,用于查找对应的升级记录。
+     */
+    default void cancelUpgradeRecordByTaskId(Long taskId) {
+        // 使用LambdaUpdateWrapper构建更新条件,将状态为“待处理”的记录更新为“已取消”
+        // TODO @li:哪些可以更新,通过 service 传递。mapper 尽量不要有逻辑
+        update(new LambdaUpdateWrapper<IotOtaUpgradeRecordDO>()
+                .set(IotOtaUpgradeRecordDO::getStatus, IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus())
+                .eq(IotOtaUpgradeRecordDO::getTaskId, taskId)
+                .eq(IotOtaUpgradeRecordDO::getStatus, IotOtaUpgradeRecordStatusEnum.PENDING.getStatus())
+        );
+    }
+
+    /**
+     * 根据状态查询符合条件的升级记录列表
+     * <p>
+     * 该函数使用LambdaQueryWrapperX构建查询条件,查询指定状态的升级记录。
+     *
+     * @param state 升级记录的状态,用于筛选符合条件的记录
+     * @return 返回符合指定状态的升级记录列表,类型为List<IotOtaUpgradeRecordDO>
+     */
+    default List<IotOtaUpgradeRecordDO> selectUpgradeRecordListByState(Integer state) {
+        // 使用LambdaQueryWrapperX构建查询条件,根据状态查询符合条件的升级记录
+        return selectList(new LambdaQueryWrapperX<IotOtaUpgradeRecordDO>()
+                .eq(IotOtaUpgradeRecordDO::getStatus, state));
+    }
+
+    /**
+     * 更新升级记录状态
+     * <p>
+     * 该函数用于批量更新指定ID列表中的升级记录状态。通过传入的ID列表和状态值,使用LambdaUpdateWrapper构建更新条件,
+     * 并执行更新操作。
+     *
+     * @param ids    需要更新的升级记录ID列表,类型为List<Long>。传入的ID列表中的记录将被更新。
+     * @param status 要更新的状态值,类型为Integer。该值将被设置到符合条件的升级记录中。
+     */
+    default void updateUpgradeRecordStatus(List<Long> ids, Integer status) {
+        // 使用LambdaUpdateWrapper构建更新条件,设置状态字段,并根据ID列表进行筛选
+        update(new LambdaUpdateWrapper<IotOtaUpgradeRecordDO>()
+                .set(IotOtaUpgradeRecordDO::getStatus, status)
+                .in(IotOtaUpgradeRecordDO::getId, ids)
+        );
+    }
+
+    /**
+     * 根据任务ID查询升级记录列表
+     * <p>
+     * 该函数通过任务ID查询符合条件的升级记录,并返回查询结果列表。
+     *
+     * @param taskId 任务ID,用于筛选升级记录
+     * @return 返回符合条件的升级记录列表,若未找到则返回空列表
+     */
+    default List<IotOtaUpgradeRecordDO> selectUpgradeRecordListByTaskId(Long taskId) {
+        // 使用LambdaQueryWrapperX构建查询条件,根据任务ID查询符合条件的升级记录
+        return selectList(new LambdaQueryWrapperX<IotOtaUpgradeRecordDO>()
+                .eq(IotOtaUpgradeRecordDO::getTaskId, taskId));
+    }
+
+}

+ 58 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.iot.dal.mysql.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+// TODO @li:这里的注释,可以去掉哈,多了点点
+/**
+ * IotOtaUpgradeTaskMapper 接口用于操作 IotOtaUpgradeTaskDO 数据库表。
+ * 该接口继承自 BaseMapperX,提供了基本的数据库操作方法。
+ */
+@Mapper
+public interface IotOtaUpgradeTaskMapper extends BaseMapperX<IotOtaUpgradeTaskDO> {
+
+    /**
+     * 根据固件ID和任务名称查询升级任务列表。
+     *
+     * @param firmwareId 固件ID,用于筛选升级任务
+     * @param name       任务名称,用于筛选升级任务
+     * @return 符合条件的升级任务列表
+     */
+    default List<IotOtaUpgradeTaskDO> selectByFirmwareIdAndName(Long firmwareId, String name) {
+        return selectList(new LambdaQueryWrapperX<IotOtaUpgradeTaskDO>()
+                .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, firmwareId)
+                .eqIfPresent(IotOtaUpgradeTaskDO::getName, name));
+    }
+
+    /**
+     * 分页查询升级任务列表,支持根据固件ID和任务名称进行筛选。
+     *
+     * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件
+     * @return 分页结果,包含符合条件的升级任务列表
+     */
+    default PageResult<IotOtaUpgradeTaskDO> selectUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<IotOtaUpgradeTaskDO>()
+                .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, pageReqVO.getFirmwareId())
+                .likeIfPresent(IotOtaUpgradeTaskDO::getName, pageReqVO.getName()));
+    }
+
+    /**
+     * 根据任务状态查询升级任务列表
+     * <p>
+     * 该函数通过传入的任务状态,查询数据库中符合条件的升级任务列表。
+     *
+     * @param status 任务状态,用于筛选升级任务的状态值
+     * @return 返回符合条件的升级任务列表,列表中的每个元素为 IotOtaUpgradeTaskDO 对象
+     */
+    default List<IotOtaUpgradeTaskDO> selectUpgradeTaskByState(Integer status) {
+        return selectList(IotOtaUpgradeTaskDO::getStatus, status);
+    }
+
+
+}

+ 35 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeRecordJob.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.job.ota;
+
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class IotOtaUpgradeRecordJob implements JobHandler {
+
+    @Resource
+    private IotOtaUpgradeRecordService upgradeRecordService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) throws Exception {
+        // 1. 查询待处理的升级记录
+        List<IotOtaUpgradeRecordDO> upgradeRecords = upgradeRecordService
+                .getUpgradeRecordListByState(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus());
+
+        // TODO @芋艿 2.执行升级动作
+        // TODO @li:应该是逐条 push,逐条更新。不用批量哈
+
+        // 3. 最终,更新升级记录状态
+        List<Long> ids = upgradeRecords.stream().map(IotOtaUpgradeRecordDO::getId).toList();
+        upgradeRecordService.updateUpgradeRecordStatus(ids, IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus());
+        return "";
+    }
+
+}

+ 72 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeTaskJob.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.iot.job.ota;
+
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService;
+import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeTaskService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+// TODO @li:也不用通过 job 去统计。可以通过 record update status 后,主动去更新 task 的状态。
+@Slf4j
+@Component
+public class IotOtaUpgradeTaskJob implements JobHandler {
+
+    @Resource
+    private IotOtaUpgradeTaskService upgradeTaskService;
+    @Resource
+    private IotOtaUpgradeRecordService upgradeRecordService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) throws Exception {
+        // 1.这个任务主要是为了检查并更新升级任务的状态
+        List<IotOtaUpgradeTaskDO> upgradeTasks = upgradeTaskService
+                .getUpgradeTaskByState(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus());
+        // 2.遍历并且确定升级任务的状态
+        upgradeTasks.forEach(this::checkUpgradeTaskState);
+        // TODO @芋艿: 其他的一些业务逻辑
+        return "";
+    }
+
+    private void checkUpgradeTaskState(IotOtaUpgradeTaskDO upgradeTask) {
+        // 1.查询任务所有的升级记录
+        List<IotOtaUpgradeRecordDO> upgradeRecords =
+                upgradeRecordService.getUpgradeRecordListByTaskId(upgradeTask.getId());
+        if (upgradeRecords.stream().anyMatch(upgradeRecord ->
+                ObjectUtils.equalsAny(upgradeRecord.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus()))) {
+            // 如果存在正在升级的升级记录,则升级任务的状态为进行中
+            log.debug("升级任务 {} 状态为进行中", upgradeTask.getId());
+        } else if (upgradeRecords.stream().allMatch(upgradeRecord ->
+                ObjectUtils.equalsAny(upgradeRecord.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.SUCCESS.getStatus()))) {
+            // 如果全部升级成功,则升级任务的状态为已完成
+            upgradeTaskService.updateUpgradeTaskStatus(upgradeTask.getId(),
+                    IotOtaUpgradeTaskStatusEnum.COMPLETED.getStatus());
+        } else if (upgradeRecords.stream().noneMatch(upgradeRecord ->
+                ObjectUtils.equalsAny(upgradeRecord.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(),
+                        IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) &&
+                upgradeRecords.stream().anyMatch(upgradeRecord ->
+                        ObjectUtils.equalsAny(upgradeRecord.getStatus(),
+                                IotOtaUpgradeRecordStatusEnum.FAILURE.getStatus(),
+                                IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus()))) {
+            // 如果全部升级完毕,但是存在升级失败或者取消的升级记录,则升级任务的状态为失败
+            upgradeTaskService.updateUpgradeTaskStatus(upgradeTask.getId(),
+                    IotOtaUpgradeTaskStatusEnum.INCOMPLETE.getStatus());
+        }
+    }
+
+}

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

@@ -35,8 +35,8 @@ public interface IotDeviceService {
      * @return 设备
      */
     IotDeviceDO createDevice(@NotEmpty(message = "产品标识不能为空") String productKey,
-            @NotEmpty(message = "设备名称不能为空") String deviceName,
-            Long gatewayId);
+                             @NotEmpty(message = "设备名称不能为空") String deviceName,
+                             Long gatewayId);
 
     /**
      * 更新设备
@@ -46,6 +46,7 @@ public interface IotDeviceService {
     void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
 
     // TODO @芋艿:先这么实现。未来看情况,要不要自己实现
+
     /**
      * 更新设备的所属网关
      *
@@ -110,7 +111,7 @@ public interface IotDeviceService {
     IotDeviceDO getDeviceByDeviceKey(String deviceKey);
 
     /**
-     * 得设备分页
+     * ��得设备分页
      *
      * @param pageReqVO 分页查询
      * @return IoT 设备分页
@@ -151,7 +152,7 @@ public interface IotDeviceService {
 
     /**
      * 【缓存】根据产品 key 和设备名称,获得设备信息
-     *
+     * <p>
      * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!!
      *
      * @param productKey 产品 key
@@ -192,4 +193,12 @@ public interface IotDeviceService {
      */
     List<IotDeviceDO> getDeviceList();
 
+    /**
+     * 获取 MQTT 连接参数
+     *
+     * @param deviceId 设备 ID
+     * @return MQTT 连接参数
+     */
+    IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId);
+
 }

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

@@ -20,6 +20,8 @@ import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
 import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
+import cn.iocoder.yudao.module.iot.util.MqttSignUtils;
+import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult;
 import jakarta.annotation.Resource;
 import jakarta.validation.ConstraintViolationException;
 import lombok.extern.slf4j.Slf4j;
@@ -99,7 +101,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
     }
 
     private void validateCreateDeviceParam(String productKey, String deviceName, String deviceKey,
-            Long gatewayId, IotProductDO product) {
+                                           Long gatewayId, IotProductDO product) {
         TenantUtils.executeIgnore(() -> {
             // 校验设备名称在同一产品下是否唯一
             if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) {
@@ -121,12 +123,8 @@ public class IotDeviceServiceImpl implements IotDeviceService {
     private void initDevice(IotDeviceDO device, IotProductDO product) {
         device.setProductId(product.getId()).setProductKey(product.getProductKey())
                 .setDeviceType(product.getDeviceType());
-        // 生成并设置必要的字段
-        // TODO @芋艿:各种 mqtt 是不是可以简化!
-        device.setDeviceSecret(generateDeviceSecret())
-                .setMqttClientId(generateMqttClientId())
-                .setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey()))
-                .setMqttPassword(generateMqttPassword());
+        // 生成密钥
+        device.setDeviceSecret(generateDeviceSecret());
         // 设置设备状态为未激活
         device.setState(IotDeviceStateEnum.INACTIVE.getState());
     }
@@ -261,6 +259,16 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         return deviceMapper.selectListByState(state);
     }
 
+    @Override
+    public List<IotDeviceDO> getDeviceListByProductId(Long productId) {
+        return deviceMapper.selectListByProductId(productId);
+    }
+
+    @Override
+    public List<IotDeviceDO> getDeviceListByIdList(List<Long> deviceIdList) {
+        return deviceMapper.selectByIds(deviceIdList);
+    }
+
     @Override
     public void updateDeviceState(Long id, Integer state) {
         // 1. 校验存在
@@ -318,35 +326,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         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) {
@@ -417,6 +396,17 @@ public class IotDeviceServiceImpl implements IotDeviceService {
         return respVO;
     }
 
+    @Override
+    public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) {
+        IotDeviceDO device = validateDeviceExists(deviceId);
+        MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(),
+                device.getDeviceSecret());
+        return new IotDeviceMqttConnectionParamsRespVO()
+                .setMqttClientId(mqttSignResult.getClientId())
+                .setMqttUsername(mqttSignResult.getUsername())
+                .setMqttPassword(mqttSignResult.getPassword());
+    }
+
     private void deleteDeviceCache(IotDeviceDO device) {
         // 保证 Spring AOP 触发
         getSelf().deleteDeviceCache0(device);

+ 7 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java

@@ -62,4 +62,11 @@ public interface IotDeviceUpstreamService {
      */
     void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO);
 
+    /**
+     * Emqx 连接认证
+     *
+     * @param authReqDTO Emqx 连接认证 DTO
+     */
+    boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO);
+
 }

+ 45 - 11
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java

@@ -20,6 +20,8 @@ import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer;
 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
 import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService;
 import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
+import cn.iocoder.yudao.module.iot.util.MqttSignUtils;
+import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -58,25 +60,26 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
         // 2.1 情况一:属性上报
         String requestId = IdUtil.fastSimpleUUID();
         if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) {
-            reportDeviceProperty(((IotDevicePropertyReportReqDTO)
-                    new IotDevicePropertyReportReqDTO().setRequestId(requestId).setReportTime(LocalDateTime.now())
-                            .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
+            reportDeviceProperty(((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO()
+                    .setRequestId(requestId).setReportTime(LocalDateTime.now())
+                    .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
                     .setProperties((Map<String, Object>) simulatorReqVO.getData()));
             return;
         }
         // 2.2 情况二:事件上报
         if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) {
-            reportDeviceEvent(((IotDeviceEventReportReqDTO)
-                    new IotDeviceEventReportReqDTO().setRequestId(requestId).setReportTime(LocalDateTime.now())
-                            .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
-                    .setIdentifier(simulatorReqVO.getIdentifier()).setParams((Map<String, Object>) simulatorReqVO.getData()));
+            reportDeviceEvent(((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId)
+                    .setReportTime(LocalDateTime.now())
+                    .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
+                    .setIdentifier(simulatorReqVO.getIdentifier())
+                    .setParams((Map<String, Object>) simulatorReqVO.getData()));
             return;
         }
         // 2.3 情况三:状态变更
         if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.STATE.getType())) {
-            updateDeviceState(((IotDeviceStateUpdateReqDTO)
-                    new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now())
-                            .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
+            updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO()
+                    .setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now())
+                    .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
                     .setState((Integer) simulatorReqVO.getData()));
             return;
         }
@@ -171,7 +174,7 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
     }
 
     private void registerDevice0(String productKey, String deviceName, Long gatewayId,
-                                 IotDeviceUpstreamAbstractReqDTO registerReqDTO) {
+            IotDeviceUpstreamAbstractReqDTO registerReqDTO) {
         // 1.1 注册设备
         IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName);
         boolean registerNew = device == null;
@@ -277,6 +280,37 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
         sendDeviceMessage(message, device);
     }
 
+    @Override
+    public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
+        log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO);
+        // 1. 校验设备是否存在
+        // username 格式:${DeviceName}&${ProductKey}
+        String[] usernameParts = authReqDTO.getUsername().split("&");
+        if (usernameParts.length != 2) {
+            log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]");
+            return false;
+        }
+        String deviceName = usernameParts[0];
+        String productKey = usernameParts[1];
+        IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(
+                productKey, deviceName);
+        if (device == null) {
+            log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]",
+                    productKey, deviceName);
+            return false;
+        }
+        // 2. 校验密码
+        String deviceSecret = device.getDeviceSecret();
+        String clientId = authReqDTO.getClientId();
+        MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId);
+        if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) {
+            log.info("[authenticateEmqxConnection][认证成功]");
+            return true;
+        }
+        log.error("[authenticateEmqxConnection][认证失败,密码不正确]");
+        return false;
+    }
+
     private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) {
         // 1. 【异步】记录设备与插件实例的映射
         pluginInstanceService.updateDevicePluginInstanceProcessIdAsync(device.getDeviceKey(), reqDTO.getProcessId());

+ 58 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import jakarta.validation.Valid;
+
+// TODO @li:类、方法注释有点冗余,可以参考别的模块哈
+/**
+ * OTA固件管理服务接口
+ * 提供OTA固件的创建、更新和查询等功能
+ */
+public interface IotOtaFirmwareService {
+
+    /**
+     * 创建OTA固件
+     *
+     * @param saveReqVO OTA固件保存请求对象,包含固件的相关信息
+     * @return 返回新创建的固件的ID
+     */
+    Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO);
+
+    /**
+     * 更新OTA固件信息
+     *
+     * @param updateReqVO OTA固件保存请求对象,包含需要更新的固件信息
+     */
+    void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO);
+
+    /**
+     * 根据ID获取OTA固件信息
+     *
+     * @param id OTA固件的唯一标识符
+     * @return 返回OTA固件的详细信息对象
+     */
+    IotOtaFirmwareDO getOtaFirmware(Long id);
+
+    /**
+     * 分页查询OTA固件信息
+     *
+     * @param pageReqVO 包含分页查询条件的请求对象
+     * @return 返回分页查询结果,包含固件信息列表和分页信息
+     */
+    PageResult<IotOtaFirmwareDO> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO);
+
+    /**
+     * 验证物联网OTA固件是否存在
+     *
+     * @param id 固件的唯一标识符
+     *           该方法用于检查系统中是否存在与给定ID关联的物联网OTA固件信息
+     *           主要目的是在进行固件更新操作前,确保目标固件已经存在并可以被访问
+     *           如果固件不存在,该方法可能抛出异常或返回错误信息,具体行为未定义
+     */
+    IotOtaFirmwareDO validateFirmwareExists(Long id);
+
+}

+ 93 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE;
+
+@Slf4j
+@Service
+@Validated
+public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService {
+
+    @Resource
+    private IotOtaFirmwareMapper otaFirmwareMapper;
+
+    @Override
+    public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) {
+        // 1. 校验固件产品 + 版本号不能重复
+        // TODO @li:需要考虑设备也存在
+        validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion());
+
+        // 2.转化数据格式,准备存储到数据库中
+        IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class);
+        otaFirmwareMapper.insert(firmware);
+        return firmware.getId();
+    }
+
+    @Override
+    public void updateOtaFirmware(IotOtaFirmwareUpdateReqVO updateReqVO) {
+        // TODO @li:如果序号只有一个,直接写 1. 更好哈
+        // 1.1. 校验存在
+        validateFirmwareExists(updateReqVO.getId());
+
+        // 2. 更新数据
+        IotOtaFirmwareDO updateObj = BeanUtils.toBean(updateReqVO, IotOtaFirmwareDO.class);
+        otaFirmwareMapper.updateById(updateObj);
+    }
+
+    @Override
+    public IotOtaFirmwareDO getOtaFirmware(Long id) {
+        return otaFirmwareMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<IotOtaFirmwareDO> getOtaFirmwarePage(IotOtaFirmwarePageReqVO pageReqVO) {
+        return otaFirmwareMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public IotOtaFirmwareDO validateFirmwareExists(Long id) {
+        IotOtaFirmwareDO firmware = otaFirmwareMapper.selectById(id);
+        if (firmware == null) {
+            throw exception(OTA_FIRMWARE_NOT_EXISTS);
+        }
+        return firmware;
+    }
+
+    /**
+     * 验证产品和版本号是否重复
+     * <p>
+     * 该方法用于确保在系统中不存在具有相同产品ID和版本号的固件条目
+     * 它通过调用otaFirmwareMapper的selectByProductIdAndVersion方法来查询数据库中是否存在匹配的产品ID和版本号的固件信息
+     * 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在,从而避免数据重复
+     *
+     * @param productId 产品ID,用于数据库查询
+     * @param version   版本号,用于数据库查询
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,提示固件信息已存在
+     */
+    private void validateProductAndVersionDuplicate(String productId, String version) {
+        // 查询数据库中是否存在具有相同产品ID和版本号的固件信息
+        List<IotOtaFirmwareDO> list = otaFirmwareMapper.selectByProductIdAndVersion(productId, version);
+        // 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在
+        // TODO @li:使用 isNotEmpty 这种 方法,简化
+        if (Objects.nonNull(list) && !list.isEmpty()) {
+            throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE);
+        }
+    }
+
+}

+ 114 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java

@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordCreateReqBO;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordUpdateReqBO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * IotOtaUpgradeRecordService 接口定义了与物联网设备OTA升级记录相关的操作。
+ * 该接口提供了创建、更新、查询、统计和重试升级记录的功能。
+ */
+public interface IotOtaUpgradeRecordService {
+
+    // TODO @createOtaUpgradeRecordBatch 哈,需要补充方法里,缺少 Ota 关键字的
+
+    /**
+     * 批量创建物联网OTA升级记录
+     * <p>
+     * 该函数用于处理一组物联网OTA升级记录的创建请求,并将这些记录批量保存到系统中。
+     *
+     * @param createList 包含多个物联网OTA升级记录创建请求的列表,每个请求对象都经过校验(@Valid注解确保)
+     *                 列表中的每个元素都是IotOtaUpgradeRecordCreateReqBO类型的对象,表示一个独立的升级记录创建请求。
+     */
+    void createUpgradeRecordBatch(@Valid List<IotOtaUpgradeRecordCreateReqBO> createList);
+
+    // TODO @li:尽量避免写比较大的通用 update。而是根据场景提供,这样才能收敛
+    /**
+     * 更新现有的 OTA 升级记录
+     *
+     * @param updateReqBO 包含更新升级记录所需信息的请求对象,必须经过验证。
+     */
+    void updateUpgradeRecord(@Valid IotOtaUpgradeRecordUpdateReqBO updateReqBO);
+
+    /**
+     * 获取OTA升级记录的数量统计。
+     *
+     * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录数量
+     */
+    Map<Integer, Long> getOtaUpgradeRecordCount(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO);
+
+    /**
+     * 获取 OTA 升级记录的统计信息。
+     *
+     * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录统计信息
+     */
+    Map<Integer, Long> getOtaUpgradeRecordStatistics(Long firmwareId);
+
+    /**
+     * 重试指定的OTA升级记录。
+     *
+     * @param id 需要重试的升级记录的ID。
+     */
+    void retryUpgradeRecord(Long id);
+
+    /**
+     * 获取指定ID的OTA升级记录的详细信息。
+     *
+     * @param id 需要查询的升级记录的ID。
+     * @return 返回包含升级记录详细信息的响应对象。
+     */
+    IotOtaUpgradeRecordDO getUpgradeRecord(Long id);
+
+    /**
+     * 分页查询OTA升级记录。
+     *
+     * @param pageReqVO 包含分页查询条件的请求对象,必须经过验证。
+     * @return 返回包含分页查询结果的响应对象。
+     */
+    PageResult<IotOtaUpgradeRecordDO> getUpgradeRecordPage(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO);
+
+    /**
+     * 根据任务ID取消升级记录。
+     * <p>
+     * 该函数用于根据给定的任务ID,取消与该任务相关的升级记录。通常用于在任务执行失败或用户手动取消时,
+     * 清理或标记相关的升级记录为取消状态。
+     *
+     * @param taskId 要取消升级记录的任务ID。该ID唯一标识一个任务,通常由任务管理系统生成。
+     */
+    void cancelUpgradeRecordByTaskId(Long taskId);
+
+    /**
+     * 根据升级状态获取升级记录列表
+     *
+     * @param state 升级状态,用于筛选符合条件的升级记录
+     * @return 返回符合指定状态的升级记录列表,列表中的元素为 {@link IotOtaUpgradeRecordDO} 对象
+     */
+    List<IotOtaUpgradeRecordDO> getUpgradeRecordListByState(Integer state);
+
+    /**
+     * 更新升级记录的状态。
+     * <p>
+     * 该函数用于批量更新指定升级记录的状态。通过传入的ID列表和状态值,将对应的升级记录的状态更新为指定的值。
+     *
+     * @param ids    需要更新状态的升级记录的ID列表。列表中的每个元素代表一个升级记录的ID。
+     * @param status 要更新的状态值。该值应为有效的状态标识符,通常为整数类型。
+     */
+    void updateUpgradeRecordStatus(List<Long> ids, Integer status);
+
+    /**
+     * 根据任务ID获取升级记录列表
+     * <p>
+     * 该函数通过给定的任务ID,查询并返回与该任务相关的所有升级记录。
+     *
+     * @param taskId 任务ID,用于指定需要查询的任务
+     * @return 返回一个包含升级记录的列表,列表中的每个元素为IotOtaUpgradeRecordDO对象
+     */
+    List<IotOtaUpgradeRecordDO> getUpgradeRecordListByTaskId(Long taskId);
+
+}

+ 212 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java

@@ -0,0 +1,212 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeRecordMapper;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordCreateReqBO;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordUpdateReqBO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
+
+@Slf4j
+@Service
+@Validated
+public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordService {
+
+    @Resource
+    private IotOtaUpgradeRecordMapper upgradeRecordMapper;
+
+    @Override
+    public void createUpgradeRecordBatch(List<IotOtaUpgradeRecordCreateReqBO> createList) {
+        // 1. 批量校验参数信息
+        createList.forEach(saveBO -> validateUpgradeRecordDuplicate(saveBO.getFirmwareId(), saveBO.getTaskId(), saveBO.getDeviceId()));
+
+        // 2. 将数据批量存储到数据库里
+        List<IotOtaUpgradeRecordDO> upgradeRecords = BeanUtils.toBean(createList, IotOtaUpgradeRecordDO.class);
+        upgradeRecordMapper.insertBatch(upgradeRecords);
+    }
+
+    @Override
+    @Transactional
+    public void updateUpgradeRecord(IotOtaUpgradeRecordUpdateReqBO updateReqBO) {
+        // 1. 校验升级记录信息是否存在
+        validateUpgradeRecordExists(updateReqBO.getId());
+
+        // 2. 将数据转化成数据库存储的格式
+        IotOtaUpgradeRecordDO updateRecord = BeanUtils.toBean(updateReqBO, IotOtaUpgradeRecordDO.class);
+        upgradeRecordMapper.updateById(updateRecord);
+        // TODO @芋艿: 更新升级记录触发的其他Action
+    }
+
+    /**
+     * 获取OTA升级记录的数量统计。
+     * 该方法根据传入的查询条件,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。
+     *
+     * @param pageReqVO 包含查询条件的请求对象,主要包括任务ID和设备名称等信息。
+     * @return 返回一个Map,其中键为状态常量,值为对应状态的记录数量。
+     */
+    @Override
+    @Transactional
+    public Map<Integer, Long> getOtaUpgradeRecordCount(IotOtaUpgradeRecordPageReqVO pageReqVO) {
+        // 分别查询不同状态的OTA升级记录数量
+        // TODO @li: 通过 groupby 统计下;
+        Long pending = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.PENDING.getStatus());
+        Long pushed = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus());
+        Long upgrading = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus());
+        Long success = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.SUCCESS.getStatus());
+        Long failure = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.FAILURE.getStatus());
+        Long canceled = upgradeRecordMapper.getOtaUpgradeRecordCount(pageReqVO.getTaskId(), pageReqVO.getDeviceName(), IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus());
+        // 将各状态的数量封装到Map中返回
+        // TODO @li:使用 MapUtil,因为 Map.of 是 jdk9 才有,后续不好同步到 master 的 jdk8;
+        return Map.of(IotOtaUpgradeRecordPageReqVO.PENDING, pending,
+                IotOtaUpgradeRecordPageReqVO.PUSHED, pushed,
+                IotOtaUpgradeRecordPageReqVO.UPGRADING, upgrading,
+                IotOtaUpgradeRecordPageReqVO.SUCCESS, success,
+                IotOtaUpgradeRecordPageReqVO.FAILURE, failure,
+                IotOtaUpgradeRecordPageReqVO.CANCELED, canceled);
+    }
+
+    /**
+     * 获取指定固件ID的OTA升级记录统计信息。
+     * 该方法通过查询数据库,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。
+     *
+     * @param firmwareId 固件ID,用于指定需要统计的固件升级记录。
+     * @return 返回一个Map,其中键为升级记录状态(如PENDING、PUSHED等),值为对应状态的记录数量。
+     */
+    @Override
+    @Transactional
+    public Map<Integer, Long> getOtaUpgradeRecordStatistics(Long firmwareId) {
+        // 查询并统计不同状态的OTA升级记录数量
+        // TODO @li: 通过 groupby 统计下;
+        Long pending = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.PENDING.getStatus());
+        Long pushed = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus());
+        Long upgrading = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus());
+        Long success = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.SUCCESS.getStatus());
+        Long failure = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.FAILURE.getStatus());
+        Long canceled = upgradeRecordMapper.getOtaUpgradeRecordStatistics(firmwareId, IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus());
+        // 将统计结果封装为Map并返回
+        return Map.of(IotOtaUpgradeRecordPageReqVO.PENDING, pending,
+                IotOtaUpgradeRecordPageReqVO.PUSHED, pushed,
+                IotOtaUpgradeRecordPageReqVO.UPGRADING, upgrading,
+                IotOtaUpgradeRecordPageReqVO.SUCCESS, success,
+                IotOtaUpgradeRecordPageReqVO.FAILURE, failure,
+                IotOtaUpgradeRecordPageReqVO.CANCELED, canceled);
+    }
+
+    @Override
+    public void retryUpgradeRecord(Long id) {
+        // 1.1.校验升级记录信息是否存在
+        IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id);
+        // 1.2.校验升级记录是否可以重新升级
+        validateUpgradeRecordCanRetry(upgradeRecord);
+
+        // 2.将一些数据重置,这样定时任务轮询就可以重启任务
+        upgradeRecordMapper.updateById(new IotOtaUpgradeRecordDO()
+                .setId(upgradeRecord.getId()).setProgress(0)
+                .setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()));
+        // TODO @芋艿: 重试升级记录触发的其他Action
+        // TODO 如果一个升级记录被取消或者已经执行失败,重试成功,是否会对升级任务的状态有影响?
+    }
+
+    @Override
+    public IotOtaUpgradeRecordDO getUpgradeRecord(Long id) {
+        return upgradeRecordMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<IotOtaUpgradeRecordDO> getUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) {
+        return upgradeRecordMapper.selectUpgradeRecordPage(pageReqVO);
+    }
+
+    @Override
+    public void cancelUpgradeRecordByTaskId(Long taskId) {
+        // 暂定只有待推送的升级记录可以取消
+        upgradeRecordMapper.cancelUpgradeRecordByTaskId(taskId);
+    }
+
+    @Override
+    public List<IotOtaUpgradeRecordDO> getUpgradeRecordListByState(Integer state) {
+        return upgradeRecordMapper.selectUpgradeRecordListByState(state);
+    }
+
+    @Override
+    public void updateUpgradeRecordStatus(List<Long> ids, Integer status) {
+        upgradeRecordMapper.updateUpgradeRecordStatus(ids, status);
+    }
+
+    @Override
+    public List<IotOtaUpgradeRecordDO> getUpgradeRecordListByTaskId(Long taskId) {
+        return upgradeRecordMapper.selectUpgradeRecordListByTaskId(taskId);
+    }
+
+    /**
+     * 验证指定的升级记录是否存在。
+     * <p>
+     * 该函数通过给定的ID查询升级记录,如果查询结果为空,则抛出异常,表示升级记录不存在。
+     *
+     * @param id 升级记录的唯一标识符,类型为Long。
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,异常类型为OTA_UPGRADE_RECORD_NOT_EXISTS。
+     */
+    private IotOtaUpgradeRecordDO validateUpgradeRecordExists(Long id) {
+        // 根据ID查询升级记录
+        IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectById(id);
+        // 如果查询结果为空,抛出异常
+        if (upgradeRecord == null) {
+            throw exception(OTA_UPGRADE_RECORD_NOT_EXISTS);
+        }
+        return upgradeRecord;
+    }
+
+    /**
+     * 验证固件升级记录是否存在。
+     * <p>
+     * 该函数通过给定的固件ID、任务ID和设备ID查询升级记录,如果查询结果为空,则抛出异常。
+     *
+     * @param firmwareId 固件ID,用于标识特定的固件版本
+     * @param taskId     任务ID,用于标识特定的升级任务
+     * @param deviceId   设备ID,用于标识特定的设备
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出OTA_UPGRADE_RECORD_NOT_EXISTS异常
+     */
+    private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) {
+        // 根据条件查询升级记录
+        IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId);
+        // 如果查询结果为空,抛出异常
+        if (upgradeRecord != null) {
+            throw exception(OTA_UPGRADE_RECORD_DUPLICATE);
+        }
+    }
+
+    /**
+     * 验证升级记录是否可以重试。
+     * <p>
+     * 该方法用于检查给定的升级记录是否处于允许重试的状态。如果升级记录的状态为
+     * PENDING、PUSHED 或 UPGRADING,则抛出异常,表示不允许重试。
+     *
+     * @param upgradeRecord 需要验证的升级记录对象,类型为 IotOtaUpgradeRecordDO
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出 OTA_UPGRADE_RECORD_CANNOT_RETRY 异常
+     */
+    private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) {
+        // 检查升级记录的状态是否为 PENDING、PUSHED 或 UPGRADING
+        if (ObjectUtils.equalsAny(upgradeRecord.getStatus(),
+                IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(),
+                IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(),
+                IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) {
+            // 如果升级记录处于上述状态之一,则抛出异常,表示不允许重试
+            throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY);
+        }
+    }
+
+}

+ 68 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+// TODO @li:类、方法注释有点冗余,可以参考别的模块哈
+/**
+ * IoT OTA升级任务服务接口
+ * 提供OTA升级任务的创建、取消和查询功能
+ */
+public interface IotOtaUpgradeTaskService {
+
+    /**
+     * 创建OTA升级任务
+     *
+     * @param createReqVO OTA升级任务的创建请求对象,包含创建任务所需的信息
+     * @return 创建成功的OTA升级任务的ID
+     */
+    Long createUpgradeTask(@Valid IotOtaUpgradeTaskSaveReqVO createReqVO);
+
+    /**
+     * 取消OTA升级任务
+     *
+     * @param id 要取消的OTA升级任务的ID
+     */
+    void cancelUpgradeTask(Long id);
+
+    /**
+     * 根据ID获取OTA升级任务的详细信息
+     *
+     * @param id OTA升级任务的ID
+     * @return OTA升级任务的详细信息对象
+     */
+    IotOtaUpgradeTaskDO getUpgradeTask(Long id);
+
+    /**
+     * 分页查询OTA升级任务
+     *
+     * @param pageReqVO OTA升级任务的分页查询请求对象,包含查询条件和分页信息
+     * @return 分页查询结果,包含OTA升级任务列表和总记录数
+     */
+    PageResult<IotOtaUpgradeTaskDO> getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO);
+
+    /**
+     * 根据任务状态获取升级任务列表
+     *
+     * @param state 任务状态,用于筛选符合条件的升级任务
+     * @return 返回符合指定状态的升级任务列表,列表中的元素为 IotOtaUpgradeTaskDO 对象
+     */
+    List<IotOtaUpgradeTaskDO> getUpgradeTaskByState(Integer state);
+
+    /**
+     * 更新升级任务的状态。
+     * <p>
+     * 该函数用于根据任务ID更新指定升级任务的状态。通常用于在任务执行过程中
+     * 更新任务的状态,例如从“进行中”变为“已完成”或“失败”。
+     *
+     * @param id     升级任务的唯一标识符,类型为Long。不能为null。
+     * @param status 要更新的任务状态,类型为Integer。通常表示任务的状态码,如0表示未开始,1表示进行中,2表示已完成等。
+     */
+    void updateUpgradeTaskStatus(Long id, Integer status);
+
+}

+ 249 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java

@@ -0,0 +1,249 @@
+package cn.iocoder.yudao.module.iot.service.ota;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO;
+import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO;
+import cn.iocoder.yudao.module.iot.convert.ota.IotOtaUpgradeRecordConvert;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeTaskMapper;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum;
+import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
+import cn.iocoder.yudao.module.iot.service.ota.bo.IotOtaUpgradeRecordCreateReqBO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
+
+@Slf4j
+@Service
+@Validated
+public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService {
+
+    @Resource
+    private IotOtaUpgradeTaskMapper upgradeTaskMapper;
+
+    @Resource
+    @Lazy
+    private IotDeviceService deviceService;
+    @Resource
+    @Lazy
+    private IotOtaFirmwareService firmwareService;
+    @Resource
+    @Lazy
+    private IotOtaUpgradeRecordService upgradeRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO) {
+        // 1.1 校验同一固件的升级任务名称不重复
+        validateFirmwareTaskDuplicate(createReqVO.getFirmwareId(), createReqVO.getName());
+        // 1.2 校验固件信息是否存在
+        IotOtaFirmwareDO firmware = firmwareService.validateFirmwareExists(createReqVO.getFirmwareId());
+        // 1.3 校验升级范围=2(指定设备时),deviceIds deviceNames不为空并且长度相等
+        // TODO @li:deviceNames 应该后端查询
+        validateScopeAndDevice(createReqVO.getScope(), createReqVO.getDeviceIds(), createReqVO.getDeviceNames());
+        // TODO @li:如果全部范围,但是没设备可以升级,需要报错
+
+        // 2. 保存 OTA 升级任务信息到数据库
+        IotOtaUpgradeTaskDO upgradeTask = initUpgradeTask(createReqVO, firmware.getProductId());
+        upgradeTaskMapper.insert(upgradeTask);
+
+        // 3. 生成设备升级记录信息并存储,等待定时任务轮询
+        List<IotOtaUpgradeRecordCreateReqBO> upgradeRecordList = initUpgradeRecordList(
+                upgradeTask, firmware, createReqVO.getDeviceIds());
+        // TODO @li:只需要传递 deviceIds、firewareId、剩余的 upgradeRecordService 里面自己处理;这样,后续 record 加字段,都不需要透传太多;解耦
+        upgradeRecordService.createUpgradeRecordBatch(upgradeRecordList);
+        // TODO @芋艿: 创建任务触发的其他Action
+        return upgradeTask.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancelUpgradeTask(Long id) {
+        // 1.1 校验升级任务是否存在
+        IotOtaUpgradeTaskDO upgradeTask = validateUpgradeTaskExists(id);
+        // 1.2 校验升级任务是否可以取消
+        // TODO @li:这种一次性的,可以不考虑拆分方法
+        validateUpgradeTaskCanCancel(upgradeTask);
+
+        // 2. 更新 OTA 升级任务状态为已取消
+        upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder()
+                .id(id).status(IotOtaUpgradeTaskStatusEnum.CANCELED.getStatus())
+                .build());
+        // 3. 更新 OTA 升级记录状态为已取消
+        upgradeRecordService.cancelUpgradeRecordByTaskId(id);
+        // TODO @芋艿: 取消任务触发的其他Action
+    }
+
+    @Override
+    public IotOtaUpgradeTaskDO getUpgradeTask(Long id) {
+        return upgradeTaskMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<IotOtaUpgradeTaskDO> getUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) {
+        return upgradeTaskMapper.selectUpgradeTaskPage(pageReqVO);
+    }
+
+    @Override
+    public List<IotOtaUpgradeTaskDO> getUpgradeTaskByState(Integer state) {
+        return upgradeTaskMapper.selectUpgradeTaskByState(state);
+    }
+
+    @Override
+    public void updateUpgradeTaskStatus(Long id, Integer status) {
+        upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder()
+                .id(id).status(status)
+                .build());
+    }
+
+    /**
+     * 校验固件升级任务是否重复
+     * <p>
+     * 该方法用于检查给定固件ID和任务名称组合是否已存在于数据库中,如果存在则抛出异常,
+     * 表示任务名称对于该固件而言是重复的此检查确保用户不能创建具有相同名称的任务,
+     * 从而避免数据重复和混淆
+     *
+     * @param firmwareId 固件的唯一标识符,用于区分不同的固件
+     * @param taskName   升级任务的名称,用于与固件ID一起检查重复性
+     * @throws cn.iocoder.yudao.framework.common.exception.ServerException 则抛出预定义的异常
+     */
+    private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) {
+        // 查询数据库中是否有相同固件ID和任务名称的升级任务存在
+        List<IotOtaUpgradeTaskDO> upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName);
+        // 如果查询结果不为空,说明存在重复的任务名称,抛出异常
+        if (CollUtil.isNotEmpty(upgradeTaskList)) {
+            throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE);
+        }
+    }
+
+    /**
+     * 验证升级任务的范围和设备参数是否有效
+     * 当选择特定设备进行升级时,确保提供的设备ID和设备名称列表有效且对应
+     *
+     * @param scope       升级任务的范围,表示是选择特定设备还是其他范围
+     * @param deviceIds   设备ID列表,用于标识参与升级的设备
+     * @param deviceNames 设备名称列表,与设备ID列表对应
+     */
+    private void validateScopeAndDevice(Integer scope, List<Long> deviceIds, List<String> deviceNames) {
+        // 当升级任务范围为选择特定设备时
+        if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) {
+            // 检查设备ID列表和设备名称列表是否为空或长度不一致,若不符合要求,则抛出异常
+            if (CollUtil.isEmpty(deviceIds) || CollUtil.isEmpty(deviceNames) || deviceIds.size() != deviceNames.size()) {
+                throw exception(OTA_UPGRADE_TASK_PARAMS_INVALID);
+            }
+        }
+    }
+
+    /**
+     * 验证升级任务是否存在
+     * <p>
+     * 通过查询数据库来验证给定ID的升级任务是否存在此方法主要用于确保后续操作所针对的升级任务是有效的
+     *
+     * @param id 升级任务的唯一标识符如果为null或数据库中不存在对应的记录,则认为任务不存在
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果升级任务不存在,则抛出异常提示任务不存在
+     */
+    private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) {
+        // 查询数据库中是否有相同固件ID和任务名称的升级任务存在
+        IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id);
+        // 如果查询结果不为空,说明存在重复的任务名称,抛出异常
+        if (Objects.isNull(upgradeTask)) {
+            throw exception(OTA_UPGRADE_TASK_NOT_EXISTS);
+        }
+        return upgradeTask;
+    }
+
+    /**
+     * 验证升级任务是否可以被取消
+     * <p>
+     * 此方法旨在确保只有当升级任务处于进行中状态时,才可以执行取消操作
+     * 它通过比较任务的当前状态与预定义的进行中状态来判断是否允许取消操作
+     * 如果任务状态不符合条件,则抛出异常,表明该任务无法取消
+     *
+     * @param upgradeTask 待验证的升级任务对象,包含任务的详细信息,如状态等
+     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果任务状态不是进行中,则抛出此异常,表明任务无法取消
+     */
+    private void validateUpgradeTaskCanCancel(IotOtaUpgradeTaskDO upgradeTask) {
+        // 检查升级任务的状态是否为进行中,只有此状态下的任务才允许取消
+        if (!Objects.equals(upgradeTask.getStatus(), IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus())) {
+            // 只有进行中的任务才可以取消
+            throw exception(OTA_UPGRADE_TASK_CANNOT_CANCEL);
+        }
+    }
+
+    // TODO @li:一次性,不复用的,可以直接写在对应的逻辑里;
+    /**
+     * 初始化升级任务
+     * <p>
+     * 根据请求参数创建升级任务对象,并根据选择的范围初始化设备数量
+     * 如果选择特定设备进行升级,则设备数量为所选设备的总数
+     * 如果选择全部设备进行升级,则设备数量为该固件对应产品下的所有设备总数
+     *
+     * @param createReqVO 升级任务保存请求对象,包含创建升级任务所需的信息
+     * @return 返回初始化后的升级任务对象
+     */
+    private IotOtaUpgradeTaskDO initUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, String productId) {
+        // 配置各项参数
+        IotOtaUpgradeTaskDO upgradeTask = IotOtaUpgradeTaskDO.builder()
+                // TODO @li:不用每个占一行。最好相同类型的,放在一行里;
+                .name(createReqVO.getName())
+                .description(createReqVO.getDescription())
+                .firmwareId(createReqVO.getFirmwareId())
+                .scope(createReqVO.getScope())
+                .deviceIds(createReqVO.getDeviceIds())
+                .deviceNames(createReqVO.getDeviceNames())
+                .deviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds())))
+                .status(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus())
+                .build();
+        // 如果选择全选,则需要查询设备数量
+        if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) {
+            // 根据产品ID查询设备数量
+            Long deviceCount = deviceService.getDeviceCountByProductId(Convert.toLong(productId));
+            // 设置升级任务的设备数量
+            upgradeTask.setDeviceCount(deviceCount);
+        }
+        // 返回初始化后的升级任务对象
+        return upgradeTask;
+    }
+
+    /**
+     * 初始化升级记录列表
+     * <p>
+     * 根据升级任务的范围(选择设备或按产品ID)获取设备列表,并将其转换为升级记录请求对象列表。
+     *
+     * @param upgradeTask 升级任务对象,包含升级任务的相关信息
+     * @param firmware    固件对象,包含固件的相关信息
+     * @param deviceIds   设备ID列表,仅在升级任务范围为选择设备时使用
+     * @return 升级记录请求对象列表,包含每个设备的升级记录信息
+     */
+    private List<IotOtaUpgradeRecordCreateReqBO> initUpgradeRecordList(
+            IotOtaUpgradeTaskDO upgradeTask, IotOtaFirmwareDO firmware, List<Long> deviceIds) {
+        // TODO @li:需要考虑,如果创建多个任务,相互之间不能重复;
+        // 1)指定设备的时候,进行校验;2)如果是全部,则过滤其它已经发起的;;;;;另外,需要排除掉 cancel 的哈。因为 cancal 之后,还可以发起
+        // 根据升级任务的范围确定设备列表
+        List<IotDeviceDO> deviceList;
+        if (Objects.equals(upgradeTask.getScope(), IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) {
+            // 如果升级任务范围为选择设备,则根据设备ID列表获取设备信息
+            deviceList = deviceService.getDeviceListByIdList(deviceIds);
+        } else {
+            // 如果升级任务范围为按产品ID,则根据固件的产品ID获取设备信息
+            deviceList = deviceService.getDeviceListByProductId(Convert.toLong(firmware.getProductId()));
+        }
+        // 将升级任务、固件和设备列表转换为升级记录请求对象列表
+        return IotOtaUpgradeRecordConvert.INSTANCE.convertBOList(upgradeTask, firmware, deviceList);
+    }
+
+}

+ 79 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/bo/IotOtaUpgradeRecordCreateReqBO.java

@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.iot.service.ota.bo;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class IotOtaUpgradeRecordCreateReqBO {
+
+    /**
+     * 固件编号
+     * <p>
+     * 关联 {@link IotOtaFirmwareDO#getId()}
+     */
+    @NotNull(message = "固件编号不能为空")
+    private Long firmwareId;
+    /**
+     * 任务编号
+     * <p>
+     * 关联 {@link IotOtaUpgradeTaskDO#getId()}
+     */
+    @NotNull(message = "任务编号不能为空")
+    private Long taskId;
+    /**
+     * 产品标识
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
+     */
+    private String productKey;
+    /**
+     * 设备名称
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
+     */
+    private String deviceName;
+    /**
+     * 设备编号
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
+     */
+    @NotNull(message = "设备编号不能为空")
+    private String deviceId;
+    /**
+     * 来源的固件编号
+     * <p>
+     * 关联 {@link IotDeviceDO#getFirmwareId()}
+     */
+    private Long fromFirmwareId;
+    /**
+     * 升级状态
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 升级进度,百分比
+     */
+    private Integer progress;
+    /**
+     * 升级进度描述
+     * <p>
+     * 注意,只记录设备最后一次的升级进度描述
+     * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志
+     */
+    private String description;
+    /**
+     * 升级开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 升级结束时间
+     */
+    private LocalDateTime endTime;
+
+}

+ 46 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/bo/IotOtaUpgradeRecordUpdateReqBO.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.iot.service.ota.bo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.Range;
+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 @li:这个类,貌似没用?
+@Data
+public class IotOtaUpgradeRecordUpdateReqBO {
+
+    /**
+     * 升级记录编号
+     */
+    @NotNull(message = "升级记录编号不能为空")
+    private Long id;
+    /**
+     * 升级状态
+     * <p>
+     * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
+     */
+    @InEnum(IotOtaUpgradeRecordStatusEnum.class)
+    private Integer status;
+    /**
+     * 升级进度,百分比
+     */
+    @Range(min = 0, max = 100, message = "升级进度必须介于 0-100 之间")
+    private Integer progress;
+    /**
+     * 升级开始时间
+     */
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime startTime;
+    /**
+     * 升级结束时间
+     */
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime endTime;
+
+}

+ 6 - 154
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java

@@ -1,51 +1,32 @@
 package cn.iocoder.yudao.module.iot.service.rule.action;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO;
-import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
 import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum;
 import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
 import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService;
+import cn.iocoder.yudao.module.iot.service.rule.action.databridge.IotDataBridgeExecute;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.rocketmq.client.producer.DefaultMQProducer;
-import org.apache.rocketmq.client.producer.SendResult;
-import org.apache.rocketmq.client.producer.SendStatus;
-import org.apache.rocketmq.common.message.Message;
-import org.apache.rocketmq.remoting.common.RemotingHelper;
-import org.springframework.http.*;
 import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
 
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-
-import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+import java.util.List;
 
 /**
  * IoT 数据桥梁的 {@link IotRuleSceneAction} 实现类
  *
  * @author 芋道源码
  */
-// TODO @芋艿:【优化】因为 bridge 会比较多,所以可以考虑在 rule 下,新建一个 bridge 的 package,然后定义一个 bridgehandler,它有:
-//    1. input 方法、output 方法
-//    2. build 方法,用于有状态的连接,例如说 mq、tcp、websocket
 @Component
 @Slf4j
 public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction {
 
-    @Resource
-    private RestTemplate restTemplate;
-
     @Resource
     private IotDataBridgeService dataBridgeService;
+    @Resource
+    private List<IotDataBridgeExecute> dataBridgeExecutes;
 
     @Override
     public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) {
@@ -65,26 +46,8 @@ public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction {
             return;
         }
 
-        // 2.1 执行 HTTP 请求
-        if (IotDataBridgTypeEnum.HTTP.getType().equals(dataBridge.getType())) {
-            executeHttp(message, (IotDataBridgeDO.HttpConfig) dataBridge.getConfig());
-            return;
-        }
-        // 2.2 执行 RocketMQ 发送消息
-        if (IotDataBridgTypeEnum.ROCKETMQ.getType().equals(dataBridge.getType())) {
-            executeRocketMQ(message, (IotDataBridgeDO.RocketMQConfig) dataBridge.getConfig());
-            return;
-        }
-
-        // TODO @芋艿:因为下面的,都是有状态的,所以通过 guava 缓存连接,然后通过 RemovalNotification 实现关闭。例如说,一次新建有效期是 10 分钟;
-        // TODO @芋艿:mq-redis
-        // TODO @芋艿:mq-数据库
-        // TODO @芋艿:kafka
-        // TODO @芋艿:rocketmq
-        // TODO @芋艿:rabbitmq
-        // TODO @芋艿:mqtt
-        // TODO @芋艿:tcp
-        // TODO @芋艿:websocket
+        // 2. 执行数据桥接操作
+        dataBridgeExecutes.forEach(execute -> execute.execute(message, dataBridge));
     }
 
     @Override
@@ -92,115 +55,4 @@ public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction {
         return IotRuleSceneActionTypeEnum.DATA_BRIDGE;
     }
 
-    @SuppressWarnings({"unchecked", "deprecation"})
-    private void executeHttp(IotDeviceMessage message, IotDataBridgeDO.HttpConfig config) {
-        String url = null;
-        HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase());
-        HttpEntity<String> requestEntity = null;
-        ResponseEntity<String> responseEntity = null;
-        try {
-            // 1.1 构建 Header
-            HttpHeaders headers = new HttpHeaders();
-            if (CollUtil.isNotEmpty(config.getHeaders())) {
-                config.getHeaders().putAll(config.getHeaders());
-            }
-            headers.add(HEADER_TENANT_ID, message.getTenantId().toString());
-            // 1.2 构建 URL
-            UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(config.getUrl());
-            if (CollUtil.isNotEmpty(config.getQuery())) {
-                config.getQuery().forEach(uriBuilder::queryParam);
-            }
-            // 1.3 构建请求体
-            if (method == HttpMethod.GET) {
-                uriBuilder.queryParam("message", HttpUtils.encodeUtf8(JsonUtils.toJsonString(message)));
-                url = uriBuilder.build().toUriString();
-                requestEntity = new HttpEntity<>(headers);
-            } else {
-                url = uriBuilder.build().toUriString();
-                Map<String, Object> requestBody = JsonUtils.parseObject(config.getBody(), Map.class);
-                if (requestBody == null) {
-                    requestBody = new HashMap<>();
-                }
-                requestBody.put("message", message);
-                headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
-                requestEntity = new HttpEntity<>(JsonUtils.toJsonString(requestBody), headers);
-            }
-
-            // 2.1 发送请求
-            responseEntity = restTemplate.exchange(url, method, requestEntity, String.class);
-            // 2.2 记录日志
-            if (responseEntity.getStatusCode().is2xxSuccessful()) {
-                log.info("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]",
-                        message, config, url, method, requestEntity, responseEntity);
-            } else {
-                log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]",
-                        message, config, url, method, requestEntity, responseEntity);
-            }
-        } catch (Exception e) {
-            log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]",
-                    message, config, url, method, requestEntity, responseEntity, e);
-        }
-    }
-
-    private void executeRocketMQ(IotDeviceMessage message, IotDataBridgeDO.RocketMQConfig config) {
-        // 1.1 创建生产者实例,指定生产者组名
-        DefaultMQProducer producer = new DefaultMQProducer(config.getGroup());
-        // TODO @puhui999:可以考虑,基于 guava 做 cache,使用 config 作为 key,然后假个 listener 超时,销毁 producer
-        try {
-            // 1.2 设置 NameServer 地址
-            producer.setNamesrvAddr(config.getNameServer());
-            // 1.3 启动生产者
-            producer.start();
-
-            // 2.1 创建消息对象,指定Topic、Tag和消息体
-            Message msg = new Message(
-                    config.getTopic(),
-                    config.getTags(),
-                    message.toString().getBytes(RemotingHelper.DEFAULT_CHARSET)
-            );
-            // 2.2 发送同步消息并处理结果
-            SendResult sendResult = producer.send(msg);
-            // 2.3 处理发送结果
-            if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
-                log.info("[executeRocketMQ][message({}) config({}) 发送成功,结果({})]", message, config, sendResult);
-            } else {
-                log.error("[executeRocketMQ][message({}) config({}) 发送失败,结果({})]", message, config, sendResult);
-            }
-        } catch (Exception e) {
-            log.error("[executeRocketMQ][message({}) config({}) 发送异常]", message, config, e);
-        } finally {
-            // 3. 关闭生产者
-            producer.shutdown();
-        }
-    }
-
-    // TODO @芋艿:测试代码,后续清理
-    public static void main(String[] args) {
-        // 1. 创建 IotRuleSceneDataBridgeAction 实例
-        IotRuleSceneDataBridgeAction action = new IotRuleSceneDataBridgeAction();
-
-        // 2. 创建测试消息
-        IotDeviceMessage message = IotDeviceMessage.builder()
-                .requestId("TEST-001")
-                .productKey("testProduct")
-                .deviceName("testDevice")
-                .deviceKey("testDeviceKey")
-                .type("property")
-                .identifier("temperature")
-                .data("{\"value\": 60}")
-                .reportTime(LocalDateTime.now())
-                .tenantId(1L)
-                .build();
-
-        // 3. 创建 RocketMQ 配置
-        IotDataBridgeDO.RocketMQConfig config = new IotDataBridgeDO.RocketMQConfig();
-        config.setNameServer("127.0.0.1:9876");
-        config.setGroup("test-group");
-        config.setTopic("test-topic");
-        config.setTags("test-tag");
-
-        // 4. 执行测试
-        action.executeRocketMQ(message, config);
-    }
-
 }

+ 32 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
+import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
+
+
+/**
+ * IoT 数据桥梁的执行器 execute 接口
+ *
+ * @author HUIHUI
+ */
+public interface IotDataBridgeExecute {
+
+    /**
+     * 执行数据桥接操作
+     *
+     * @param message    设备消息
+     * @param dataBridge 数据桥梁
+     */
+    void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge);
+
+    // TODO @芋艿:因为下面的,都是有状态的,所以通过 guava 缓存连接,然后通过 RemovalNotification 实现关闭。例如说,一次新建有效期是 10 分钟;
+    // TODO @芋艿:mq-redis
+    // TODO @芋艿:mq-数据库
+    // TODO @芋艿:kafka
+    // TODO @芋艿:rocketmq
+    // TODO @芋艿:rabbitmq
+    // TODO @芋艿:mqtt
+    // TODO @芋艿:tcp
+    // TODO @芋艿:websocket
+
+}

+ 93 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
+import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
+import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Http 的 {@link IotDataBridgeExecute} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class IotHttpDataBridgeExecute implements IotDataBridgeExecute {
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Override
+    public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
+        // 1.1 校验数据桥接的类型 == HTTP
+        if (!IotDataBridgTypeEnum.HTTP.getType().equals(dataBridge.getType())) {
+            return;
+        }
+        // 1.2 执行 HTTP 请求
+        executeHttp(message, (IotDataBridgeDO.HttpConfig) dataBridge.getConfig());
+    }
+
+    @SuppressWarnings({"unchecked", "deprecation"})
+    private void executeHttp(IotDeviceMessage message, IotDataBridgeDO.HttpConfig config) {
+        String url = null;
+        HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase());
+        HttpEntity<String> requestEntity = null;
+        ResponseEntity<String> responseEntity = null;
+        try {
+            // 1.1 构建 Header
+            HttpHeaders headers = new HttpHeaders();
+            if (CollUtil.isNotEmpty(config.getHeaders())) {
+                config.getHeaders().putAll(config.getHeaders());
+            }
+            headers.add(HEADER_TENANT_ID, message.getTenantId().toString());
+            // 1.2 构建 URL
+            UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(config.getUrl());
+            if (CollUtil.isNotEmpty(config.getQuery())) {
+                config.getQuery().forEach(uriBuilder::queryParam);
+            }
+            // 1.3 构建请求体
+            if (method == HttpMethod.GET) {
+                uriBuilder.queryParam("message", HttpUtils.encodeUtf8(JsonUtils.toJsonString(message)));
+                url = uriBuilder.build().toUriString();
+                requestEntity = new HttpEntity<>(headers);
+            } else {
+                url = uriBuilder.build().toUriString();
+                Map<String, Object> requestBody = JsonUtils.parseObject(config.getBody(), Map.class);
+                if (requestBody == null) {
+                    requestBody = new HashMap<>();
+                }
+                requestBody.put("message", message);
+                headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
+                requestEntity = new HttpEntity<>(JsonUtils.toJsonString(requestBody), headers);
+            }
+
+            // 2.1 发送请求
+            responseEntity = restTemplate.exchange(url, method, requestEntity, String.class);
+            // 2.2 记录日志
+            if (responseEntity.getStatusCode().is2xxSuccessful()) {
+                log.info("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]",
+                        message, config, url, method, requestEntity, responseEntity);
+            } else {
+                log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]",
+                        message, config, url, method, requestEntity, responseEntity);
+            }
+        } catch (Exception e) {
+            log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]",
+                    message, config, url, method, requestEntity, responseEntity, e);
+        }
+    }
+
+}

+ 131 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java

@@ -0,0 +1,131 @@
+package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
+
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
+import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
+import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.client.producer.SendStatus;
+import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.remoting.common.RemotingHelper;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.concurrent.Executors;
+
+/**
+ * RocketMQ 的 {@link IotDataBridgeExecute} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class IotRocketMQDataBridgeExecute implements IotDataBridgeExecute {
+
+    /**
+     * 针对 {@link IotDataBridgeDO.RocketMQConfig} 的 DefaultMQProducer 缓存
+     */
+    // TODO @puhui999:因为 kafka 之类也存在这个情况,是不是得搞个抽象类。提供一个 initProducer,和 closeProducer 方法
+    private final LoadingCache<IotDataBridgeDO.RocketMQConfig, DefaultMQProducer> PRODUCER_CACHE = CacheBuilder.newBuilder()
+            .refreshAfterWrite(Duration.ofMinutes(10)) // TODO puhui999:应该是 read 30 分钟哈
+            // 增加移除监听器,自动关闭 producer
+            .removalListener(notification -> {
+                DefaultMQProducer producer = (DefaultMQProducer) notification.getValue();
+                // TODO puhui999:if return,更简短哈
+                if (producer != null) {
+                    try {
+                        producer.shutdown();
+                        log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey());
+                    } catch (Exception e) {
+                        log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 关闭失败]", notification.getKey(), e);
+                    }
+                }
+            })
+            // TODO @puhui999:就同步哈,不用异步处理。
+            // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+            .build(CacheLoader.asyncReloading(new CacheLoader<IotDataBridgeDO.RocketMQConfig, DefaultMQProducer>() {
+
+                @Override
+                public DefaultMQProducer load(IotDataBridgeDO.RocketMQConfig config) throws Exception {
+                    DefaultMQProducer producer = new DefaultMQProducer(config.getGroup());
+                    producer.setNamesrvAddr(config.getNameServer());
+                    producer.start();
+                    log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已创建并启动]", config);
+                    return producer;
+                }
+
+            }, Executors.newCachedThreadPool()));
+
+    @Override
+    public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
+        // 1.1 校验数据桥接的类型 == ROCKETMQ
+        if (!IotDataBridgTypeEnum.ROCKETMQ.getType().equals(dataBridge.getType())) {
+            return;
+        }
+        // 1.2 执行 RocketMQ 发送消息
+        executeRocketMQ(message, (IotDataBridgeDO.RocketMQConfig) dataBridge.getConfig());
+    }
+
+    private void executeRocketMQ(IotDeviceMessage message, IotDataBridgeDO.RocketMQConfig config) {
+        try {
+            // 1. 获取或创建 Producer
+            DefaultMQProducer producer = PRODUCER_CACHE.get(config);
+
+            // 2.1 创建消息对象,指定Topic、Tag和消息体
+            Message msg = new Message(
+                    config.getTopic(),
+                    config.getTags(),
+                    message.toString().getBytes(RemotingHelper.DEFAULT_CHARSET)
+            );
+            // 2.2 发送同步消息并处理结果
+            SendResult sendResult = producer.send(msg);
+            // 2.3 处理发送结果
+            if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
+                log.info("[executeRocketMQ][message({}) config({}) 发送成功,结果({})]", message, config, sendResult);
+            } else {
+                log.error("[executeRocketMQ][message({}) config({}) 发送失败,结果({})]", message, config, sendResult);
+            }
+        } catch (Exception e) {
+            log.error("[executeRocketMQ][message({}) config({}) 发送异常]", message, config, e);
+        }
+    }
+
+    // TODO @芋艿:测试代码,后续清理
+    public static void main(String[] args) {
+        // 1. 创建一个共享的实例
+        IotRocketMQDataBridgeExecute action = new IotRocketMQDataBridgeExecute();
+
+        // 2. 创建共享的配置
+        IotDataBridgeDO.RocketMQConfig config = new IotDataBridgeDO.RocketMQConfig();
+        config.setNameServer("127.0.0.1:9876");
+        config.setGroup("test-group");
+        config.setTopic("test-topic");
+        config.setTags("test-tag");
+
+        // 3. 创建共享的消息
+        IotDeviceMessage message = IotDeviceMessage.builder()
+                .requestId("TEST-001")
+                .productKey("testProduct")
+                .deviceName("testDevice")
+                .deviceKey("testDeviceKey")
+                .type("property")
+                .identifier("temperature")
+                .data("{\"value\": 60}")
+                .reportTime(LocalDateTime.now())
+                .tenantId(1L)
+                .build();
+
+        // 4. 执行两次测试,验证缓存
+        log.info("[main][第一次执行,应该会创建新的 producer]");
+        action.executeRocketMQ(message, config);
+
+        log.info("[main][第二次执行,应该会复用缓存的 producer]");
+        action.executeRocketMQ(message, config);
+    }
+
+}

+ 67 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.iot.util;
+
+import cn.hutool.crypto.digest.HMac;
+import cn.hutool.crypto.digest.HmacAlgorithm;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * MQTT 签名工具类
+ *
+ * 提供静态方法来计算 MQTT 连接参数
+ */
+public class MqttSignUtils {
+
+    /**
+     * 计算 MQTT 连接参数
+     *
+     * @param productKey   产品密钥
+     * @param deviceName   设备名称
+     * @param deviceSecret 设备密钥
+     * @return 包含 clientId, username, password 的结果对象
+     */
+    public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) {
+        return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName);
+    }
+
+    /**
+     * 计算 MQTT 连接参数
+     *
+     * @param productKey   产品密钥
+     * @param deviceName   设备名称
+     * @param deviceSecret 设备密钥
+     * @param clientId     客户端 ID
+     * @return 包含 clientId, username, password 的结果对象
+     */
+    public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) {
+        String username = deviceName + "&" + productKey;
+        // 构建签名内容
+        StringBuilder signContentBuilder = new StringBuilder()
+                .append("clientId").append(clientId)
+                .append("deviceName").append(deviceName)
+                .append("deviceSecret").append(deviceSecret)
+                .append("productKey").append(productKey);
+
+        // 使用 HMac 计算签名
+        byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8);
+        String signContent = signContentBuilder.toString();
+        HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key);
+        String password = mac.digestHex(signContent);
+
+        return new MqttSignResult(clientId, username, password);
+    }
+
+    /**
+     * MQTT 签名结果类
+     */
+    @Getter
+    @AllArgsConstructor
+    public static class MqttSignResult {
+        private final String clientId;
+        private final String username;
+        private final String password;
+    }
+
+}

+ 24 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/ota/IotOtaUpgradeRecordMapper.xml

@@ -0,0 +1,24 @@
+<?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.ota.IotOtaUpgradeRecordMapper">
+
+    <!-- TODO @li:看看是不是可以通过 mybatis plus 写哈,更好适配多 db -->
+    <select id="getOtaUpgradeRecordCount" resultType="java.lang.Long">
+        select count(*)
+        from iot_ota_upgrade_record
+        where task_id = #{taskId}
+          and device_name like concat('%', #{deviceName}, '%')
+          and status = #{status}
+    </select>
+
+    <!-- TODO @li:看看是不是可以通过 mybatis plus 写哈,更好适配多 db -->
+    <select id="getOtaUpgradeRecordStatistics" resultType="java.lang.Long">
+        select count(*)
+        from iot_ota_upgrade_record
+        where firmware_id = #{firmwareId}
+          and status = #{status}
+    </select>
+
+</mapper>

+ 2 - 2
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java

@@ -44,8 +44,8 @@ public class IotPluginCommonAutoConfiguration {
 
     @Bean(initMethod = "init", destroyMethod = "stop")
     public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(
-            IotDeviceUpstreamApi deviceDataApi, IotDeviceDownstreamServer deviceDownstreamServer) {
-        return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer);
+            IotDeviceUpstreamApi deviceDataApi, IotDeviceDownstreamServer deviceDownstreamServer, IotPluginCommonProperties commonProperties) {
+        return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer, commonProperties);
     }
 
 }

+ 11 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java

@@ -7,6 +7,11 @@ import org.springframework.validation.annotation.Validated;
 
 import java.time.Duration;
 
+/**
+ * IoT 插件的通用配置类
+ *
+ * @author haohao
+ */
 @ConfigurationProperties(prefix = "yudao.iot.plugin.common")
 @Validated
 @Data
@@ -45,4 +50,10 @@ public class IotPluginCommonProperties {
      */
     private Integer downstreamPort = DOWNSTREAM_PORT_RANDOM;
 
+    /**
+     * 插件包标识符
+     */
+    @NotEmpty(message = "插件包标识符不能为空")
+    private String pluginKey;
+
 }

+ 1 - 1
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java

@@ -16,7 +16,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
 /**
- * IOT 设备配置设置 Vertx Handler
+ * IoT 设备配置设置 Vertx Handler
  *
  * 芋道源码
  */

+ 6 - 1
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java

@@ -5,14 +5,19 @@ import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceOt
 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
 import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
 import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.RoutingContext;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import io.vertx.core.json.JsonObject;
 
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
+/**
+ * IoT 设备 OTA 升级 Vertx Handler
+ * <p>
+ * 芋道源码
+ */
 @Slf4j
 @RequiredArgsConstructor
 public class IotDeviceOtaUpgradeVertxHandler implements Handler<RoutingContext> {

+ 1 - 1
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java

@@ -16,7 +16,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
 /**
- * IOT 设备服务获取 Vertx Handler
+ * IoT 设备服务获取 Vertx Handler
  *
  * 芋道源码
  */

+ 1 - 1
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java

@@ -16,7 +16,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
 /**
- * IOT 设备服务设置 Vertx Handler
+ * IoT 设置设备属性 Vertx Handler
  *
  * 芋道源码
  */

+ 3 - 2
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java

@@ -5,17 +5,18 @@ import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceSe
 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
 import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
 import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.RoutingContext;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import io.vertx.core.json.JsonObject;
+
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
 /**
- * IOT 设备服务调用 Vertx Handler
+ * IoT 设备服务调用 Vertx Handler
  *
  * 芋道源码
  */

+ 3 - 2
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java

@@ -4,6 +4,7 @@ import cn.hutool.system.SystemUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
+import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties;
 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer;
 import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
 import lombok.RequiredArgsConstructor;
@@ -23,6 +24,7 @@ public class IotPluginInstanceHeartbeatJob {
 
     private final IotDeviceUpstreamApi deviceUpstreamApi;
     private final IotDeviceDownstreamServer deviceDownstreamServer;
+    private final IotPluginCommonProperties commonProperties;
 
     public void init() {
         CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true));
@@ -41,9 +43,8 @@ public class IotPluginInstanceHeartbeatJob {
     }
 
     private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) {
-        // TODO @haohao:pluginKey 的获取???
         return new IotPluginInstanceHeartbeatReqDTO()
-                .setPluginKey("yudao-module-iot-plugin-http").setProcessId(IotPluginCommonUtils.getProcessId())
+                .setPluginKey(commonProperties.getPluginKey()).setProcessId(IotPluginCommonUtils.getProcessId())
                 .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort())
                 .setOnline(online);
     }

+ 6 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java

@@ -57,6 +57,12 @@ public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi {
         return null;
     }
 
+    @Override
+    public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
+        String url = properties.getUpstreamUrl() + URL_PREFIX + "/authenticate-emqx-connection";
+        return doPost(url, authReqDTO);
+    }
+
     @Override
     public CommonResult<Boolean> reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) {
         String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-property";

+ 8 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java

@@ -41,4 +41,12 @@ public class IotPluginCommonUtils {
                 .end(JsonUtils.toJsonString(result));
     }
 
+    @SuppressWarnings("deprecation")
+    public static void writeJson(RoutingContext routingContext, String result) {
+        routingContext.response()
+                .setStatusCode(200)
+                .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
+                .end(result);
+    }
+
 }

+ 4 - 4
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties

@@ -1,6 +1,6 @@
-plugin.id=plugin-emqx
-plugin.class=cn.iocoder.yudao.module.iot.plugin.EmqxPlugin
+plugin.id=yudao-module-iot-plugin-emqx
+plugin.class=cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin
 plugin.version=1.0.0
-plugin.provider=ahh
+plugin.provider=yudao
 plugin.dependencies=
-plugin.description=plugin-emqx-1.0.0
+plugin.description=yudao-module-iot-plugin-emqx-1.0.0

+ 59 - 55
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml

@@ -12,6 +12,7 @@
     <packaging>jar</packaging>
 
     <artifactId>yudao-module-iot-plugin-emqx</artifactId>
+    <version>1.0.0</version>
 
     <name>${project.artifactId}</name>
     <description>
@@ -21,36 +22,16 @@
     <properties>
         <!-- 插件相关 -->
         <plugin.id>emqx-plugin</plugin.id>
-        <plugin.class>cn.iocoder.yudao.module.iot.plugin.EmqxPlugin</plugin.class>
-        <plugin.version>0.0.1</plugin.version>
-        <plugin.provider>ahh</plugin.provider>
-        <plugin.description>emqx-plugin-0.0.1</plugin.description>
+        <plugin.class>cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin</plugin.class>
+        <plugin.version>${project.version}</plugin.version>
+        <plugin.provider>yudao</plugin.provider>
+        <plugin.description>${project.artifactId}-${project.version}</plugin.description>
         <plugin.dependencies/>
     </properties>
 
     <build>
         <plugins>
-            <!-- DOESN'T WORK WITH MAVEN 3 (I defined the plugin metadata in properties section)
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>properties-maven-plugin</artifactId>
-                <version>1.0-alpha-2</version>
-                <executions>
-                  <execution>
-                    <phase>initialize</phase>
-                    <goals>
-                      <goal>read-project-properties</goal>
-                    </goals>
-                    <configuration>
-                      <files>
-                        <file>plugin.properties</file>
-                      </files>
-                    </configuration>
-                  </execution>
-                </executions>
-            </plugin>
-            -->
-
+            <!-- 插件模式 zip -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-antrun-plugin</artifactId>
@@ -94,6 +75,7 @@
                 </executions>
             </plugin>
 
+            <!-- 插件模式 jar -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
@@ -111,54 +93,76 @@
                     </archive>
                 </configuration>
             </plugin>
+            <!--            <plugin>-->
+            <!--                <groupId>org.apache.maven.plugins</groupId>-->
+            <!--                <artifactId>maven-shade-plugin</artifactId>-->
+            <!--                <version>3.6.0</version>-->
+            <!--                <executions>-->
+            <!--                    <execution>-->
+            <!--                        <phase>package</phase>-->
+            <!--                        <goals>-->
+            <!--                            <goal>shade</goal>-->
+            <!--                        </goals>-->
+            <!--                        <configuration>-->
+            <!--                            <minimizeJar>true</minimizeJar>-->
+            <!--                        </configuration>-->
+            <!--                    </execution>-->
+            <!--                </executions>-->
+            <!--                <configuration>-->
+            <!--                    <archive>-->
+            <!--                        <manifestEntries>-->
+            <!--                            <Plugin-Id>${plugin.id}</Plugin-Id>-->
+            <!--                            <Plugin-Class>${plugin.class}</Plugin-Class>-->
+            <!--                            <Plugin-Version>${plugin.version}</Plugin-Version>-->
+            <!--                            <Plugin-Provider>${plugin.provider}</Plugin-Provider>-->
+            <!--                            <Plugin-Description>${plugin.description}</Plugin-Description>-->
+            <!--                            <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>-->
+            <!--                        </manifestEntries>-->
+            <!--                    </archive>-->
+            <!--                </configuration>-->
+            <!--            </plugin>-->
 
+            <!-- 独立模式 -->
             <plugin>
-                <artifactId>maven-deploy-plugin</artifactId>
-                <configuration>
-                    <skip>true</skip>
-                </configuration>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                        <configuration>
+                            <classifier>-standalone</classifier>
+                        </configuration>
+                    </execution>
+                </executions>
             </plugin>
+
         </plugins>
     </build>
 
     <dependencies>
-        <!-- 其他依赖项 -->
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-web</artifactId>
-        </dependency>
-        <!-- PF4J Spring 集成 -->
-        <dependency>
-            <groupId>org.pf4j</groupId>
-            <artifactId>pf4j-spring</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <!-- 项目依赖 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-iot-api</artifactId>
+            <artifactId>yudao-module-iot-plugin-common</artifactId>
             <version>${revision}</version>
         </dependency>
+
+        <!-- Web 相关 -->
         <dependency>
-            <groupId>org.projectlombok</groupId>
-            <artifactId>lombok</artifactId>
-            <version>${lombok.version}</version>
-            <scope>provided</scope>
-        </dependency>
-        <!-- Vert.x 核心依赖 -->
-        <dependency>
-            <groupId>io.vertx</groupId>
-            <artifactId>vertx-core</artifactId>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
-        <!-- Vert.x Web 模块 -->
+
+        <!-- 工具类相关 -->
         <dependency>
             <groupId>io.vertx</groupId>
             <artifactId>vertx-web</artifactId>
         </dependency>
-        <!-- MQTT -->
         <dependency>
-            <groupId>org.eclipse.paho</groupId>
-            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-mqtt</artifactId>
         </dependency>
     </dependencies>
 </project>

+ 0 - 42
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/EmqxPlugin.java

@@ -1,42 +0,0 @@
-package cn.iocoder.yudao.module.iot.plugin;
-
-import cn.hutool.extra.spring.SpringUtil;
-import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
-import lombok.extern.slf4j.Slf4j;
-import org.pf4j.Plugin;
-import org.pf4j.PluginWrapper;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-@Slf4j
-public class EmqxPlugin extends Plugin {
-
-    private ExecutorService executorService;
-
-    public EmqxPlugin(PluginWrapper wrapper) {
-        super(wrapper);
-        this.executorService = Executors.newSingleThreadExecutor();
-    }
-
-    @Override
-    public void start() {
-        log.info("EmqxPlugin.start()");
-
-        if (executorService.isShutdown() || executorService.isTerminated()) {
-            executorService = Executors.newSingleThreadExecutor();
-        }
-
-        IotDeviceUpstreamApi deviceDataApi = SpringUtil.getBean(IotDeviceUpstreamApi.class);
-        if (deviceDataApi == null) {
-            log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
-            return;
-        }
-
-    }
-
-    @Override
-    public void stop() {
-        log.info("EmqxPlugin.stop()");
-    }
-}

+ 22 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * IoT Emqx 插件的独立运行入口
+ */
+@Slf4j
+@SpringBootApplication
+public class IotEmqxPluginApplication {
+
+    public static void main(String[] args) {
+        SpringApplication application = new SpringApplication(IotEmqxPluginApplication.class);
+        application.setWebApplicationType(WebApplicationType.NONE);
+        application.run(args);
+        log.info("[main][独立模式启动完成]");
+    }
+
+}

+ 57 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.config;
+
+import cn.hutool.extra.spring.SpringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.pf4j.PluginWrapper;
+import org.pf4j.spring.SpringPlugin;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+// TODO @芋艿:完善注释
+
+/**
+ * 负责插件的启动和停止
+ */
+@Slf4j
+public class IotEmqxPlugin extends SpringPlugin {
+
+    public IotEmqxPlugin(PluginWrapper wrapper) {
+        super(wrapper);
+    }
+
+    @Override
+    public void start() {
+        log.info("[EmqxPlugin][EmqxPlugin 插件启动开始...]");
+        try {
+
+            log.info("[EmqxPlugin][EmqxPlugin 插件启动成功...]");
+        } catch (Exception e) {
+            log.error("[EmqxPlugin][EmqxPlugin 插件开启动异常...]", e);
+        }
+    }
+
+    @Override
+    public void stop() {
+        log.info("[EmqxPlugin][EmqxPlugin 插件停止开始...]");
+        try {
+            log.info("[EmqxPlugin][EmqxPlugin 插件停止成功...]");
+        } catch (Exception e) {
+            log.error("[EmqxPlugin][EmqxPlugin 插件停止异常...]", e);
+        }
+    }
+
+    @Override
+    protected ApplicationContext createApplicationContext() {
+        // 创建插件自己的 ApplicationContext
+        AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext();
+        // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用)
+        pluginContext.setParent(SpringUtil.getApplicationContext());
+        // 继续使用插件自己的 ClassLoader 以加载插件内部的类
+        pluginContext.setClassLoader(getWrapper().getPluginClassLoader());
+        // 扫描当前插件的自动配置包
+        pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.emqx.config");
+        pluginContext.refresh();
+        return pluginContext;
+    }
+
+}

+ 31 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.config;
+
+import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
+import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
+import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl;
+import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * IoT 插件 Emqx 的专用自动配置类
+ *
+ * @author haohao
+ */
+@Configuration
+@EnableConfigurationProperties(IotPluginEmqxProperties.class)
+public class IotPluginEmqxAutoConfiguration {
+
+    @Bean(initMethod = "start", destroyMethod = "stop")
+    public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
+                                                        IotPluginEmqxProperties emqxProperties) {
+        return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi);
+    }
+
+    @Bean
+    public IotDeviceDownstreamHandler deviceDownstreamHandler() {
+        return new IotDeviceDownstreamHandlerImpl();
+    }
+
+}

+ 49 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * 物联网插件 - EMQX 配置
+ *
+ * @author 芋道源码
+ */
+@ConfigurationProperties(prefix = "yudao.iot.plugin.emqx")
+@Validated
+@Data
+public class IotPluginEmqxProperties {
+
+    /**
+     * 服务主机
+     */
+    private String mqttHost;
+    /**
+     * 服务端口
+     */
+    private int mqttPort;
+    /**
+     * 服务用户名
+     */
+    private String mqttUsername;
+
+    /**
+     * 服务密码
+     */
+    private String mqttPassword;
+    /**
+     * 是否启用 SSL
+     */
+    private boolean mqttSsl;
+
+    /**
+     * 订阅的主题
+     */
+    private String mqttTopics;
+
+    /**
+     * 认证端口
+     */
+    private int authPort;
+
+}

+ 46 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.downstream;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
+import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
+
+/**
+ * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类
+ * <p>
+ *
+ * @author 芋道源码
+ */
+public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
+
+    @Override
+    public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {
+        // 设备服务调用
+        // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}
+        // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply
+        return CommonResult.success(true);
+    }
+
+    @Override
+    public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
+        return CommonResult.success(true);
+    }
+
+    @Override
+    public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {
+        // 设置设备属性 标准 JSON
+        // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set
+        // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
+        return CommonResult.success(true);
+    }
+
+    @Override
+    public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
+        return CommonResult.success(true);
+    }
+
+    @Override
+    public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
+        return CommonResult.success(true);
+    }
+
+}

+ 164 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java

@@ -0,0 +1,164 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.upstream;
+
+import cn.hutool.core.util.IdUtil;
+import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
+import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties;
+import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler;
+import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.mqtt.MqttClient;
+import io.vertx.mqtt.MqttClientOptions;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
+ * <p>
+ * 协议:HTTP、MQTT
+ *
+ * @author haohao
+ */
+@Slf4j
+public class IotDeviceUpstreamServer {
+
+    private static final int RECONNECT_DELAY = 5000; // 重连延迟时间(毫秒)
+
+    private final Vertx vertx;
+    private final HttpServer server;
+    private final MqttClient client;
+    private final IotPluginEmqxProperties emqxProperties;
+    private final IotDeviceMqttMessageHandler mqttMessageHandler;
+
+    public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties,
+                                   IotDeviceUpstreamApi deviceUpstreamApi) {
+        this.emqxProperties = emqxProperties;
+
+        // 创建 Vertx 实例
+        this.vertx = Vertx.vertx();
+        // 创建 Router 实例
+        Router router = Router.router(vertx);
+        router.route().handler(BodyHandler.create()); // 处理 Body
+        router.post(IotDeviceAuthVertxHandler.PATH)
+                .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
+        // 创建 HttpServer 实例
+        this.server = vertx.createHttpServer().requestHandler(router);
+
+        // 创建 MQTT 客户端
+        MqttClientOptions options = new MqttClientOptions()
+                .setClientId("yudao-iot-server-" + IdUtil.fastSimpleUUID())
+                .setUsername(emqxProperties.getMqttUsername())
+                .setPassword(emqxProperties.getMqttPassword())
+                .setSsl(emqxProperties.isMqttSsl());
+        client = MqttClient.create(vertx, options);
+        this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client);
+    }
+
+    /**
+     * 启动 HTTP 服务器、MQTT 客户端
+     */
+    public void start() {
+        // 1. 启动 HTTP 服务器
+        log.info("[start][开始启动]");
+        server.listen(emqxProperties.getAuthPort())
+                .toCompletionStage()
+                .toCompletableFuture()
+                .join();
+        log.info("[start][HTTP服务器启动完成,端口({})]", this.server.actualPort());
+
+        // 2. 连接 MQTT Broker
+        connectMqtt();
+
+        // 3. 添加 MQTT 断开重连监听器
+        client.closeHandler(v -> {
+            log.warn("[closeHandler][MQTT 连接已断开,准备重连]");
+            reconnectWithDelay();
+        });
+
+        // 4. 设置 MQTT 消息处理器
+        setupMessageHandler();
+    }
+
+    /**
+     * 设置 MQTT 消息处理器
+     */
+    private void setupMessageHandler() {
+        client.publishHandler(mqttMessageHandler::handle);
+    }
+
+    /**
+     * 重连 MQTT 客户端
+     */
+    private void reconnectWithDelay() {
+        vertx.setTimer(RECONNECT_DELAY, id -> {
+            log.info("[reconnectWithDelay][开始重新连接 MQTT]");
+            connectMqtt();
+        });
+    }
+
+    /**
+     * 连接 MQTT Broker 并订阅主题
+     */
+    private void connectMqtt() {
+        client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost())
+                .onSuccess(connAck -> {
+                    log.info("[connectMqtt][MQTT客户端连接成功]");
+                    subscribeToTopics();
+                })
+                .onFailure(err -> {
+                    log.error("[connectMqtt][连接 MQTT Broker 失败]", err);
+                    reconnectWithDelay();
+                });
+    }
+
+    /**
+     * 订阅设备上行消息主题
+     */
+    private void subscribeToTopics() {
+        String[] topics = emqxProperties.getMqttTopics().split(",");
+        for (String topic : topics) {
+            client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
+                    .onSuccess(v -> log.info("[subscribeToTopics][成功订阅主题: {}]", topic))
+                    .onFailure(err -> log.error("[subscribeToTopics][订阅主题失败: {}]", topic, err));
+        }
+        log.info("[subscribeToTopics][开始订阅设备上行消息主题]");
+    }
+
+    /**
+     * 停止所有
+     */
+    public void stop() {
+        log.info("[stop][开始关闭]");
+        try {
+            // 关闭 HTTP 服务器
+            if (server != null) {
+                server.close()
+                        .toCompletionStage()
+                        .toCompletableFuture()
+                        .join();
+            }
+
+            // 关闭 MQTT 客户端
+            if (client != null) {
+                client.disconnect()
+                        .toCompletionStage()
+                        .toCompletableFuture()
+                        .join();
+            }
+
+            // 关闭 Vertx 实例
+            if (vertx != null) {
+                vertx.close()
+                        .toCompletionStage()
+                        .toCompletableFuture()
+                        .join();
+            }
+            log.info("[stop][关闭完成]");
+        } catch (Exception e) {
+            log.error("[stop][关闭异常]", e);
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 54 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO;
+import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
+import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT Emqx 连接认证的 Vert.x Handler
+ * <a href="https://docs.emqx.com/zh/emqx/latest/access-control/authn/http.html">...</a>
+ *
+ * @author haohao
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class IotDeviceAuthVertxHandler implements Handler<RoutingContext> {
+
+    public static final String PATH = "/mqtt/auth";
+
+    private final IotDeviceUpstreamApi deviceUpstreamApi;
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handle(RoutingContext routingContext) {
+
+        JsonObject json = routingContext.body().asJsonObject();
+        String clientId = json.getString("clientid");
+        String username = json.getString("username");
+        String password = json.getString("password");
+
+        IotDeviceEmqxAuthReqDTO authReqDTO = buildDeviceEmqxAuthReqDTO(clientId, username, password);
+
+        CommonResult<Boolean> authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
+        if (authResult.getCode() != 0 || !authResult.getData()) {
+            denyAccess(routingContext);
+            return;
+        }
+        IotPluginCommonUtils.writeJson(routingContext, "{\"result\": \"allow\"}");
+    }
+
+    private void denyAccess(RoutingContext routingContext) {
+        IotPluginCommonUtils.writeJson(routingContext, "{\"result\": \"deny\"}");
+    }
+
+    private IotDeviceEmqxAuthReqDTO buildDeviceEmqxAuthReqDTO(String clientId, String username, String password) {
+        return new IotDeviceEmqxAuthReqDTO().setClientId(clientId).setUsername(username).setPassword(password);
+    }
+
+}

+ 194 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java

@@ -0,0 +1,194 @@
+package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
+import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.mqtt.MqttClient;
+import io.vertx.mqtt.messages.MqttPublishMessage;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.LocalDateTime;
+
+/**
+ * IoT 设备 MQTT 消息处理器
+ * <p>
+ * 参考:
+ * <p>
+ * "<a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services?spm=a2c4g.11186623.0.0.97a72915vRck44#section-g4j-5zg-12b">...</a>">
+ */
+@Slf4j
+public class IotDeviceMqttMessageHandler {
+
+    // 设备上报属性 标准 JSON
+    // 请求Topic:/sys/${productKey}/${deviceName}/thing/event/property/post
+    // 响应Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply
+    // 设备上报事件 标准 JSON
+    // 请求Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post
+    // 响应Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply
+
+    private static final String SYS_TOPIC_PREFIX = "/sys/";
+    private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
+    private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
+    private static final String EVENT_POST_TOPIC_SUFFIX = "/post";
+
+    private final IotDeviceUpstreamApi deviceUpstreamApi;
+    private final MqttClient mqttClient;
+
+    public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) {
+        this.deviceUpstreamApi = deviceUpstreamApi;
+        this.mqttClient = mqttClient;
+    }
+
+    public void handle(MqttPublishMessage message) {
+        String topic = message.topicName();
+        String payload = message.payload().toString();
+        log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload);
+
+        try {
+            handleMessage(topic, payload);
+        } catch (Exception e) {
+            log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e);
+        }
+    }
+
+    private void handleMessage(String topic, String payload) {
+        // 校验前缀
+        if (!topic.startsWith(SYS_TOPIC_PREFIX)) {
+            log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
+            return;
+        }
+
+        // 处理设备属性上报消息
+        if (topic.endsWith(PROPERTY_POST_TOPIC)) {
+            log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic);
+            handlePropertyPost(topic, payload);
+            return;
+        }
+
+        // 处理设备事件上报消息
+        if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) {
+            log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic);
+            handleEventPost(topic, payload);
+            return;
+        }
+
+        // 未知消息类型
+        log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
+    }
+
+    /**
+     * 处理设备属性上报消息
+     *
+     * @param topic   主题
+     * @param payload 消息内容
+     */
+    private void handlePropertyPost(String topic, String payload) {
+        // 解析消息内容
+        JSONObject jsonObject = JSONUtil.parseObj(payload);
+        String[] topicParts = topic.split("/");
+
+        // 构建设备属性上报请求对象
+        IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts);
+
+        // 调用上游 API 处理设备上报数据
+        deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
+        log.info("[handlePropertyPost][处理设备上行消息成功][topic: {}][reportReqDTO: {}]",
+                topic, JSONUtil.toJsonStr(reportReqDTO));
+
+        // 发送响应消息
+        String replyTopic = topic + "_reply";
+        JSONObject response = new JSONObject()
+                .set("id", jsonObject.getStr("id"))
+                .set("code", 200)
+                .set("data", new JSONObject())
+                .set("message", "success")
+                .set("method", "thing.event.property.post");
+
+        mqttClient.publish(replyTopic,
+                Buffer.buffer(response.toString()),
+                MqttQoS.AT_LEAST_ONCE,
+                false,
+                false);
+        log.info("[handlePropertyPost][发送响应消息成功][topic: {}][response: {}]",
+                replyTopic, response.toString());
+    }
+
+    /**
+     * 处理设备事件上报消息
+     *
+     * @param topic   主题
+     * @param payload 消息内容
+     */
+    private void handleEventPost(String topic, String payload) {
+        // 解析消息内容
+        JSONObject jsonObject = JSONUtil.parseObj(payload);
+        String[] topicParts = topic.split("/");
+
+        // 构建设备事件上报请求对象
+        IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts);
+
+        // 调用上游 API 处理设备上报数据
+        deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
+        log.info("[handleEventPost][处理设备上行消息成功][topic: {}][reportReqDTO: {}]",
+                topic, JSONUtil.toJsonStr(reportReqDTO));
+
+        // 发送响应消息
+        String replyTopic = topic + "_reply";
+        String eventIdentifier = topicParts[6]; // 从 topic 中获取事件标识符
+        JSONObject response = new JSONObject()
+                .set("id", jsonObject.getStr("id"))
+                .set("code", 200)
+                .set("data", new JSONObject())
+                .set("message", "success")
+                .set("method", "thing.event." + eventIdentifier + ".post");
+
+        mqttClient.publish(replyTopic,
+                Buffer.buffer(response.toString()),
+                MqttQoS.AT_LEAST_ONCE,
+                false,
+                false);
+        log.info("[handleEventPost][发送响应消息成功][topic: {}][response: {}]",
+                replyTopic, response.toString());
+    }
+
+    /**
+     * 构建设备属性上报请求对象
+     *
+     * @param jsonObject 消息内容
+     * @param topicParts 主题部分
+     * @return 设备属性上报请求对象
+     */
+    private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject,
+                                                                 String[] topicParts) {
+        return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO()
+                .setRequestId(jsonObject.getStr("id"))
+                .setProcessId(IotPluginCommonUtils.getProcessId())
+                .setReportTime(LocalDateTime.now())
+                .setProductKey(topicParts[2])
+                .setDeviceName(topicParts[3]))
+                .setProperties(jsonObject.getJSONObject("params"));
+    }
+
+    /**
+     * 构建设备事件上报请求对象
+     *
+     * @param jsonObject 消息内容
+     * @param topicParts 主题部分
+     * @return 设备事件上报请求对象
+     */
+    private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
+        return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO()
+                .setRequestId(jsonObject.getStr("id"))
+                .setProcessId(IotPluginCommonUtils.getProcessId())
+                .setReportTime(LocalDateTime.now())
+                .setProductKey(topicParts[2])
+                .setDeviceName(topicParts[3]))
+                .setIdentifier(topicParts[4])
+                .setParams(jsonObject.getJSONObject("params"));
+    }
+}

+ 19 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml

@@ -0,0 +1,19 @@
+spring:
+  application:
+    name: yudao-module-iot-plugin-emqx
+
+yudao:
+  iot:
+    plugin:
+      common:
+        upstream-url: http://127.0.0.1:48080
+        downstream-port: 8100
+        plugin-key: yudao-module-iot-plugin-emqx
+      emqx:
+        mqtt-host: 127.0.0.1
+        mqtt-port: 1883
+        mqtt-ssl: false
+        mqtt-username: yudao
+        mqtt-password: 123456
+        mqtt-topics: "/sys/#"
+        auth-port: 8101

+ 1 - 1
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceEventReportVertxHandler.java

@@ -21,7 +21,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
 
 /**
- * IoT 设备设备上报的 Vert.x Handler
+ * IoT 设备事件上报的 Vert.x Handler
  */
 @RequiredArgsConstructor
 @Slf4j

+ 1 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml

@@ -8,5 +8,6 @@ yudao:
       common:
         upstream-url: http://127.0.0.1:48080
         downstream-port: 8093
+        plugin-key: yudao-module-iot-plugin-http
       http:
         server-port: 8092

+ 0 - 17
yudao-server/src/main/resources/application-local.yaml

@@ -267,23 +267,6 @@ justauth:
     timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟
 
 --- #################### iot相关配置 TODO 芋艿:再瞅瞅 ####################
-iot:
-  emq:
-    # 账号
-    username: haohao
-    # 密码
-    password: ahh@123456
-    # 主机地址
-    hostUrl: tcp://chaojiniu.top:1883
-    # 客户端Id,不能相同,采用随机数 ${random.value}
-    client-id: ${random.int}
-    # 默认主题
-    default-topic: test
-    # 保持连接
-    keepalive: 60
-    # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
-    clearSession: true
-
 pf4j:
 #  pluginsDir: /tmp/
   pluginsDir: ../plugins