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
This commit is contained in:
alwayssuper 2025-02-26 08:36:27 +08:00
commit 9f8c6a944c
74 changed files with 3400 additions and 363 deletions

View File

@ -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>

View File

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

View File

@ -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);
// ========== 插件相关 ==========
/**

View File

@ -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;
}

View File

@ -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 @li1_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 @li1_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, "升级记录不能重试");
}

View File

@ -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>

View File

@ -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

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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 @liget 接口不是 @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));
}
}

View File

@ -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;
}

View File

@ -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 @liswagger 注解写在 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 @liproductId 即可 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>
* 例如说MD5SHA256
*/
@Schema(description = "签名方式", example = "MD5")
private String signMethod;
// TODO @lifileSignfileSize 通过后端下载文件计算出来对前端屏蔽这个细节
/**
* 固件文件签名
*/
@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;
}

View File

@ -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;
}

View File

@ -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>
* 例如说MD5SHA256
*/
@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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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"));
}
/**

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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 "";
}
}

View File

@ -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());
}
}
}

View File

@ -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);
/**
* 得设备分页
* <EFBFBD><EFBFBD>得设备分页
*
* @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);
}

View File

@ -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);

View File

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

View File

@ -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());

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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其中键为升级记录状态如PENDINGPUSHED等值为对应状态的记录数量
*/
@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>
* 该方法用于检查给定的升级记录是否处于允许重试的状态如果升级记录的状态为
* PENDINGPUSHED UPGRADING则抛出异常表示不允许重试
*
* @param upgradeRecord 需要验证的升级记录对象类型为 IotOtaUpgradeRecordDO
* @throws cn.iocoder.yudao.framework.common.exception.ServiceException则抛出 OTA_UPGRADE_RECORD_CANNOT_RETRY 异常
*/
private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) {
// 检查升级记录的状态是否为 PENDINGPUSHED UPGRADING
if (ObjectUtils.equalsAny(upgradeRecord.getStatus(),
IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(),
IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(),
IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) {
// 如果升级记录处于上述状态之一则抛出异常表示不允许重试
throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY);
}
}
}

View File

@ -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);
}

View File

@ -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 @lideviceNames 应该后端查询
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只需要传递 deviceIdsfirewareId剩余的 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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 方法用于有状态的连接例如说 mqtcpwebsocket
@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 创建消息对象指定TopicTag和消息体
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);
}
}

View File

@ -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
}

View File

@ -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);
}
}
}

View File

@ -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 puhui999if 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 创建消息对象指定TopicTag和消息体
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);
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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
*
* 芋道源码
*/

View File

@ -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> {

View File

@ -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
*
* 芋道源码
*/

View File

@ -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
*
* 芋道源码
*/

View File

@ -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
*
* 芋道源码
*/

View File

@ -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 @haohaopluginKey 的获取
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);
}

View File

@ -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";

View File

@ -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);
}
}

View File

@ -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

View File

@ -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>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-plugin-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- Web 相关 -->
<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>
<version>${revision}</version>
</dependency>
<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>
</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>

View File

@ -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()");
}
}

View File

@ -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][独立模式启动完成]");
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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>
* 协议HTTPMQTT
*
* @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);
}
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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