feat:【IoT 物联网】优化固件相关请求和响应对象,添加文件 URL 格式校验,更新固件 ID 类型为 Long

This commit is contained in:
YunaiV
2025-06-30 19:08:29 +08:00
parent 3ca4cf265a
commit f9d782c701
12 changed files with 131 additions and 225 deletions

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.URL;
@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") @Schema(description = "管理后台 - IoT OTA 固件创建 Request VO")
@Data @Data
@@ -22,17 +23,11 @@ public class IotOtaFirmwareCreateReqVO {
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空") @NotNull(message = "产品编号不能为空")
private String productId; private Long productId;
@Schema(description = "签名方式", example = "MD5") @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.zip")
// TODO @li是不是必传哈
private String signMethod;
@Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
@NotEmpty(message = "固件文件 URL 不能为空") @NotEmpty(message = "固件文件 URL 不能为空")
@URL(message = "固件文件 URL 格式错误")
private String fileUrl; private String fileUrl;
@Schema(description = "自定义信息,建议使用 JSON 格式", example = "{\"key1\":\"value1\",\"key2\":\"value2\"}")
private String information;
} }

View File

@@ -3,21 +3,24 @@ package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Data
@Schema(description = "管理后台 - IoT OTA 固件分页 Request VO") @Schema(description = "管理后台 - IoT OTA 固件分页 Request VO")
@Data
public class IotOtaFirmwarePageReqVO extends PageParam { public class IotOtaFirmwarePageReqVO extends PageParam {
/**
* 固件名称
*/
@Schema(description = "固件名称", example = "智能开关固件") @Schema(description = "固件名称", example = "智能开关固件")
private String name; private String name;
/**
* 产品标识
*/
@Schema(description = "产品标识", example = "1024") @Schema(description = "产品标识", example = "1024")
private String productId; private String productId;
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
} }

View File

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

View File

@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@@ -13,9 +12,7 @@ public class IotOtaFirmwareUpdateReqVO {
@NotNull(message = "固件编号不能为空") @NotNull(message = "固件编号不能为空")
private Long id; private Long id;
// TODO @liname 是不是可以飞必传哈 @Schema(description = "固件名称", example = "智能开关固件")
@Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
@NotEmpty(message = "固件名称不能为空")
private String name; private String name;
@Schema(description = "固件描述", example = "某品牌型号固件,测试用") @Schema(description = "固件描述", example = "某品牌型号固件,测试用")

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.ota; package cn.iocoder.yudao.module.iot.dal.dataobject.ota;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
@@ -34,7 +35,7 @@ public class IotOtaFirmwareDO extends BaseDO {
*/ */
private String name; private String name;
/** /**
* 固件版本 * 固件描述
*/ */
private String description; private String description;
/** /**
@@ -47,37 +48,25 @@ public class IotOtaFirmwareDO extends BaseDO {
* *
* 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
*/ */
// TODO @li帮我改成 Long 哈,写错了 private Long productId;
private String productId;
/**
* 产品标识
*
* 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
*/
private String productKey;
/** /**
* 签名方式 * 固件文件 URL
*
* 例如说MD5、SHA256
*/ */
private String signMethod; private String fileUrl;
/**
* 固件文件签名
*/
private String fileSign;
/** /**
* 固件文件大小 * 固件文件大小
*/ */
private Long fileSize; private Long fileSize;
/** /**
* 固件文件 URL * 固件文件签名算法
*
* 枚举 {@link DigestAlgorithm},目前只使用 MD5
*/ */
private String fileUrl; private String fileDigestAlgorithm;
/** /**
* 自定义信息,建议使用 JSON 格式 * 固件文件签名结果
*/ */
private String information; private String fileDigestValue;
} }

View File

@@ -9,7 +9,6 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
// TODO @li参考 IotOtaUpgradeRecordMapper 的写法
@Mapper @Mapper
public interface IotOtaFirmwareMapper extends BaseMapperX<IotOtaFirmwareDO> { public interface IotOtaFirmwareMapper extends BaseMapperX<IotOtaFirmwareDO> {
@@ -20,21 +19,16 @@ public interface IotOtaFirmwareMapper extends BaseMapperX<IotOtaFirmwareDO> {
* @param version 固件版本号,用于筛选固件信息。 * @param version 固件版本号,用于筛选固件信息。
* @return 返回符合条件的固件信息列表。 * @return 返回符合条件的固件信息列表。
*/ */
default List<IotOtaFirmwareDO> selectByProductIdAndVersion(String productId, String version) { default List<IotOtaFirmwareDO> selectByProductIdAndVersion(Long productId, String version) {
return selectList(IotOtaFirmwareDO::getProductId, productId, return selectList(IotOtaFirmwareDO::getProductId, productId,
IotOtaFirmwareDO::getVersion, version); IotOtaFirmwareDO::getVersion, version);
} }
/**
* 分页查询固件信息支持根据名称和产品ID进行筛选并按创建时间降序排列。
*
* @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件。
* @return 返回分页查询结果,包含符合条件的固件信息列表。
*/
default PageResult<IotOtaFirmwareDO> selectPage(IotOtaFirmwarePageReqVO pageReqVO) { default PageResult<IotOtaFirmwareDO> selectPage(IotOtaFirmwarePageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<IotOtaFirmwareDO>() return selectPage(pageReqVO, new LambdaQueryWrapperX<IotOtaFirmwareDO>()
.likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName()) .likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName())
.eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId()) .eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId())
.betweenIfPresent(IotOtaFirmwareDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(IotOtaFirmwareDO::getCreateTime)); .orderByDesc(IotOtaFirmwareDO::getCreateTime));
} }

View File

@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwa
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
// TODO @li注释写的有点冗余可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈
/** /**
* OTA 固件管理 Service * OTA 固件管理 Service
* *
@@ -18,41 +17,39 @@ public interface IotOtaFirmwareService {
/** /**
* 创建 OTA 固件 * 创建 OTA 固件
* *
* @param saveReqVO OTA固件保存请求对象包含固件的相关信息 * @param saveReqVO 固件信息
* @return 返回新创建的固件的ID * @return 固件编号
*/ */
Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO); Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO);
/** /**
* 更新 OTA 固件信息 * 更新 OTA 固件信息
* *
* @param updateReqVO OTA固件保存请求对象包含需要更新的固件信息 * @param updateReqVO 固件信息
*/ */
void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO); void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO);
/** /**
* 根据 ID 获取 OTA 固件信息 * 根据 ID 获取 OTA 固件信息
* *
* @param id OTA固件的唯一标识符 * @param id OTA 固件编号
* @return 返回OTA固件的详细信息对象 * @return 固件信息
*/ */
IotOtaFirmwareDO getOtaFirmware(Long id); IotOtaFirmwareDO getOtaFirmware(Long id);
/** /**
* 分页查询 OTA 固件信息 * 分页查询 OTA 固件信息
* *
* @param pageReqVO 包含分页查询条件的请求对象 * @param pageReqVO 分页查询条件
* @return 返回分页查询结果,包含固件信息列表和分页信息 * @return 分页结果
*/ */
PageResult<IotOtaFirmwareDO> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO); PageResult<IotOtaFirmwareDO> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO);
/** /**
* 验证物联网 OTA 固件是否存在 * 验证物联网 OTA 固件是否存在
* *
* @param id 固件的唯一标识符 * @param id 物联网 OTA 固件编号
* 该方法用于检查系统中是否存在与给定ID关联的物联网OTA固件信息 * @return OTA 固件
* 主要目的是在进行固件更新操作前,确保目标固件已经存在并可以被访问
* 如果固件不存在,该方法可能抛出异常或返回错误信息,具体行为未定义
*/ */
IotOtaFirmwareDO validateFirmwareExists(Long id); IotOtaFirmwareDO validateFirmwareExists(Long id);

View File

@@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.iot.service.ota; package cn.iocoder.yudao.module.iot.service.ota;
import cn.hutool.core.collection.CollUtil; import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.core.convert.Convert; import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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.IotOtaFirmwareCreateReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; 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.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper; import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper;
import cn.iocoder.yudao.module.iot.service.product.IotProductService; import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -17,8 +17,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.util.List; import java.io.ByteArrayInputStream;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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_NOT_EXISTS;
@@ -37,16 +36,20 @@ public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService {
@Override @Override
public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) { public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) {
// 1. 校验固件产品 + 版本号不能重复 // 1.1 校验固件产品 + 版本号不能重复
validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion()); validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion());
// 1.2 校验产品存在
productService.validateProductExists(saveReqVO.getProductId());
// 2.1.转化数据格式,准备存储到数据库中 // 2. 构建对象 + 存储
IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class); IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class);
// 2.2.查询ProductKey // 2.1 计算文件签名等属性
// TODO @liproductService.getProduct(Convert.toLong(firmware.getProductId())) 放到 1. 后面先做参考校验。逻辑两段1先参数校验2构建对象 + 存储 try {
IotProductDO product = productService.getProduct(Convert.toLong(firmware.getProductId())); calculateFileDigest(firmware);
firmware.setProductKey(Objects.requireNonNull(product).getProductKey()); } catch (Exception e) {
// TODO @芋艿: 附件、附件签名等属性的计算 log.error("[createOtaFirmware][url({}) 计算文件签名失败]", firmware.getFileUrl(), e);
throw new RuntimeException("计算文件签名失败: " + e.getMessage());
}
otaFirmwareMapper.insert(firmware); otaFirmwareMapper.insert(firmware);
return firmware.getId(); return firmware.getId();
} }
@@ -80,25 +83,34 @@ public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService {
return firmware; return firmware;
} }
// TODO @li注释有点冗余
/** /**
* 验证产品和版本号是否重复 * 验证产品和版本号是否重复
* <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) { private void validateProductAndVersionDuplicate(Long productId, String version) {
// 查询数据库中是否存在具有相同产品ID和版本号的固件信息 // 查询1条记录检查是否存在
List<IotOtaFirmwareDO> list = otaFirmwareMapper.selectByProductIdAndVersion(productId, version); IotOtaFirmwareDO firmware = otaFirmwareMapper.selectOne(IotOtaFirmwareDO::getProductId, productId,
// 如果查询结果非空且不为null则抛出异常提示固件信息已存在 IotOtaFirmwareDO::getVersion, version);
if (CollUtil.isNotEmpty(list)) { if (firmware != null) {
throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE); throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE);
} }
} }
/**
* 计算文件签名
*
* @param firmware 固件对象
* @throws Exception 下载或计算签名失败时抛出异常
*/
private void calculateFileDigest(IotOtaFirmwareDO firmware) throws Exception {
String fileUrl = firmware.getFileUrl();
// 下载文件并计算签名
byte[] fileBytes = HttpUtil.downloadBytes(fileUrl);
// 设置文件大小
firmware.setFileSize((long) fileBytes.length);
// 计算 MD5 签名
firmware.setFileDigestAlgorithm(DigestAlgorithm.MD5.getValue());
String md5Hex = DigestUtil.digester(firmware.getFileDigestAlgorithm()).digestHex(new ByteArrayInputStream(fileBytes));
firmware.setFileDigestValue(md5Hex);
}
} }

View File

@@ -34,24 +34,19 @@ public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordServic
@Resource @Resource
private IotOtaUpgradeRecordMapper upgradeRecordMapper; private IotOtaUpgradeRecordMapper upgradeRecordMapper;
// TODO @li1@Resource 写在 @Lazy 之前先用关键注解2有必要的情况下在写 @Lazy 注解。
@Lazy
@Resource @Resource
private IotDeviceService deviceService; private IotDeviceService deviceService;
@Lazy
@Resource @Resource
private IotOtaFirmwareService firmwareService; private IotOtaFirmwareService firmwareService;
@Lazy
@Resource @Resource
private IotOtaUpgradeTaskService upgradeTaskService; private IotOtaUpgradeTaskService upgradeTaskService;
@Override @Override
public void createOtaUpgradeRecordBatch(List<Long> deviceIds, Long firmwareId, Long upgradeTaskId) { public void createOtaUpgradeRecordBatch(List<Long> deviceIds, Long firmwareId, Long upgradeTaskId) {
// 1. 校验升级记录信息是否存在,并且已经取消的任务可以重新开始 // 1. 校验升级记录信息是否存在,并且已经取消的任务可以重新开始
// TODO @li批量查询。。
deviceIds.forEach(deviceId -> validateUpgradeRecordDuplicate(firmwareId, upgradeTaskId, String.valueOf(deviceId))); deviceIds.forEach(deviceId -> validateUpgradeRecordDuplicate(firmwareId, upgradeTaskId, String.valueOf(deviceId)));
// 2.初始化OTA升级记录列表信息 // 2. 初始化OTA升级记录列表信息
IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(upgradeTaskId); IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(upgradeTaskId);
IotOtaFirmwareDO firmware = firmwareService.getOtaFirmware(firmwareId); IotOtaFirmwareDO firmware = firmwareService.getOtaFirmware(firmwareId);
List<IotDeviceDO> deviceList = deviceService.getDeviceListByIdList(deviceIds); List<IotDeviceDO> deviceList = deviceService.getDeviceListByIdList(deviceIds);
@@ -67,10 +62,9 @@ public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordServic
upgradeRecord.setProgress(0); upgradeRecord.setProgress(0);
return upgradeRecord; return upgradeRecord;
}).toList(); }).toList();
// 3.保存数据 // 3. 保存数据
upgradeRecordMapper.insertBatch(upgradeRecordList); upgradeRecordMapper.insertBatch(upgradeRecordList);
// TODO @芋艿:在这里需要处理推送升级任务的逻辑 // TODO @芋艿:在这里需要处理推送升级任务的逻辑
} }
// TODO @li1方法注释简单写2父类写了注释子类就不用写了。。。 // TODO @li1方法注释简单写2父类写了注释子类就不用写了。。。
@@ -116,9 +110,9 @@ public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordServic
@Override @Override
public void retryUpgradeRecord(Long id) { public void retryUpgradeRecord(Long id) {
// 1.1.校验升级记录信息是否存在 // 1.1 校验升级记录信息是否存在
IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id); IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id);
// 1.2.校验升级记录是否可以重新升级 // 1.2 校验升级记录是否可以重新升级
validateUpgradeRecordCanRetry(upgradeRecord); validateUpgradeRecordCanRetry(upgradeRecord);
// 2. 将一些数据重置,这样定时任务轮询就可以重启任务 // 2. 将一些数据重置,这样定时任务轮询就可以重启任务
@@ -191,16 +185,12 @@ public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordServic
* @param deviceId 设备ID用于标识特定的设备 * @param deviceId 设备ID用于标识特定的设备
*/ */
private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) { private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) {
// 根据条件查询升级记录
IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId); IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId);
// 如果查询到升级记录且状态不是已取消,则抛出异常 if (upgradeRecord == null) {
// TODO @liif return减少括号层级 return;
// TODO @liObjUtil.notEquals尽量不用 !取否逻辑; }
if (upgradeRecord != null) { if (!Objects.equals(upgradeRecord.getStatus(), IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus())) {
if (!IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus().equals(upgradeRecord.getStatus())) { throw exception(OTA_UPGRADE_RECORD_DUPLICATE);
// TODO @li提示的时候需要把 deviceName 给提示出来,不然用户不知道哪个重复啦。
throw exception(OTA_UPGRADE_RECORD_DUPLICATE);
}
} }
} }
@@ -216,12 +206,10 @@ public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordServic
*/ */
// TODO @li这种一次性的方法不复用的其实一步一定要抽成小方法 // TODO @li这种一次性的方法不复用的其实一步一定要抽成小方法
private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) { private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) {
// 检查升级记录的状态是否为 PENDING、PUSHED 或 UPGRADING
if (ObjectUtils.equalsAny(upgradeRecord.getStatus(), if (ObjectUtils.equalsAny(upgradeRecord.getStatus(),
IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(), IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(),
IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(), IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(),
IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) { IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) {
// 如果升级记录处于上述状态之一,则抛出异常,表示不允许重试
throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY); throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY);
} }
} }

View File

@@ -27,10 +27,14 @@ import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
// TODO @li完善注释、注解顺序 /**
@Slf4j * IoT OTA升级任务 Service 实现类
*
* @author Shelly Chan
*/
@Service @Service
@Validated @Validated
@Slf4j
public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService { public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService {
@Resource @Resource
@@ -105,102 +109,65 @@ public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService {
upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder().id(id).status(status).build()); upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder().id(id).status(status).build());
} }
// TODO @li注释有点冗余
/** /**
* 校验固件升级任务是否重复 * 校验固件升级任务是否重复
* <p>
* 该方法用于检查给定固件ID和任务名称组合是否已存在于数据库中如果存在则抛出异常
* 表示任务名称对于该固件而言是重复的此检查确保用户不能创建具有相同名称的任务,
* 从而避免数据重复和混淆
*
* @param firmwareId 固件的唯一标识符,用于区分不同的固件
* @param taskName 升级任务的名称用于与固件ID一起检查重复性
* @throws cn.iocoder.yudao.framework.common.exception.ServerException 则抛出预定义的异常
*/ */
private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) { private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) {
// 查询数据库中是否有相同固件ID和任务名称的升级任务存在
List<IotOtaUpgradeTaskDO> upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName); List<IotOtaUpgradeTaskDO> upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName);
// 如果查询结果不为空,说明存在重复的任务名称,抛出异常
if (CollUtil.isNotEmpty(upgradeTaskList)) { if (CollUtil.isNotEmpty(upgradeTaskList)) {
throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE); throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE);
} }
} }
// TODO @li注释有点冗余
/** /**
* 验证升级任务的范围和设备列表的有效性。 * 验证升级任务的范围和设备列表的有效性。
* <p>
* 根据升级任务的范围scope验证设备列表deviceIds或产品IDproductId是否有效。
* 如果范围是“选择设备”SELECT则必须提供设备列表如果范围是“所有设备”ALL则必须根据产品ID获取设备列表并确保列表不为空。
* *
* @param scope 升级任务的范围,参考 IotOtaUpgradeTaskScopeEnum 枚举值 * @param scope 升级任务的范围,参考 IotOtaUpgradeTaskScopeEnum 枚举值
* @param deviceIds 设备ID列表当范围为选择设备时,该列表不能为空 * @param deviceIds 设备ID列表当范围为"选择设备"时,该列表不能为空
* @param productId 产品ID当范围为所有设备时,用于获取设备列表 * @param productId 产品ID当范围为"所有设备"时,用于获取设备列表
* @throws cn.iocoder.yudao.framework.common.exception.ServiceException抛出相应的异常 * @throws cn.iocoder.yudao.framework.common.exception.ServiceException抛出相应的异常
*/ */
private void validateScopeAndDevice(Integer scope, List<Long> deviceIds, String productId) { private void validateScopeAndDevice(Integer scope, List<Long> deviceIds, Long productId) {
// TODO @liif return
// 验证范围为“选择设备”时,设备列表不能为空
if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) { if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) {
if (CollUtil.isEmpty(deviceIds)) { if (CollUtil.isEmpty(deviceIds)) {
throw exception(OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY); throw exception(OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY);
} }
} else if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { return;
// 验证范围为“所有设备”时根据产品ID获取的设备列表不能为空 }
List<IotDeviceDO> deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId));
if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.ALL.getScope())) {
List<IotDeviceDO> deviceList = deviceService.getDeviceListByProductId(productId);
if (CollUtil.isEmpty(deviceList)) { if (CollUtil.isEmpty(deviceList)) {
throw exception(OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY); throw exception(OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY);
} }
} }
} }
// TODO @li注释有点冗余
/** /**
* 验证升级任务是否存在 * 验证升级任务是否存在
* <p>
* 通过查询数据库来验证给定ID的升级任务是否存在此方法主要用于确保后续操作所针对的升级任务是有效的
*
* @param id 升级任务的唯一标识符如果为null或数据库中不存在对应的记录则认为任务不存在
* @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果升级任务不存在,则抛出异常提示任务不存在
*/ */
private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) { private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) {
// 查询数据库中是否有相同固件ID和任务名称的升级任务存在
IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id); IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id);
// 如果查询结果不为空,说明存在重复的任务名称,抛出异常
if (Objects.isNull(upgradeTask)) { if (Objects.isNull(upgradeTask)) {
throw exception(OTA_UPGRADE_TASK_NOT_EXISTS); throw exception(OTA_UPGRADE_TASK_NOT_EXISTS);
} }
return upgradeTask; return upgradeTask;
} }
// TODO @li注释有点冗余
/** /**
* 初始化升级任务 * 初始化升级任务
* <p>
* 根据请求参数创建升级任务对象,并根据选择的范围初始化设备数量
* 如果选择特定设备进行升级,则设备数量为所选设备的总数
* 如果选择全部设备进行升级,则设备数量为该固件对应产品下的所有设备总数
*
* @param createReqVO 升级任务保存请求对象,包含创建升级任务所需的信息
* @return 返回初始化后的升级任务对象
*/ */
// TODO @li一次性的方法不用特别抽小方法 private IotOtaUpgradeTaskDO initOtaUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, Long productId) {
private IotOtaUpgradeTaskDO initOtaUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, String productId) {
// 将请求参数转换为升级任务对象
IotOtaUpgradeTaskDO upgradeTask = BeanUtils.toBean(createReqVO, IotOtaUpgradeTaskDO.class); IotOtaUpgradeTaskDO upgradeTask = BeanUtils.toBean(createReqVO, IotOtaUpgradeTaskDO.class);
// 初始化的时候,设置设备数量和状态
upgradeTask.setDeviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds()))) upgradeTask.setDeviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds())))
.setStatus(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus()); .setStatus(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus());
// 如果选择全选,则需要查询设备数量
if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) {
// 根据产品ID查询设备数量 List<IotDeviceDO> deviceList = deviceService.getDeviceListByProductId(productId);
List<IotDeviceDO> deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId));
// 设置升级任务的设备数量
upgradeTask.setDeviceCount((long) deviceList.size()); upgradeTask.setDeviceCount((long) deviceList.size());
upgradeTask.setDeviceIds( upgradeTask.setDeviceIds(
deviceList.stream().map(IotDeviceDO::getId).collect(Collectors.toList())); deviceList.stream().map(IotDeviceDO::getId).collect(Collectors.toList()));
} }
// 返回初始化后的升级任务对象
return upgradeTask; return upgradeTask;
} }

View File

@@ -70,7 +70,6 @@ public class IotEmqxDownstreamHandler {
private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) { private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
// 1. 判断是否为回复消息 // 1. 判断是否为回复消息
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
// 2. 根据消息方法类型构建对应的主题 // 2. 根据消息方法类型构建对应的主题
return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
} }

View File

@@ -18,6 +18,11 @@ public final class IotMqttTopicUtils {
*/ */
private static final String SYS_TOPIC_PREFIX = "/sys/"; private static final String SYS_TOPIC_PREFIX = "/sys/";
/**
* 回复主题后缀
*/
private static final String REPLY_TOPIC_SUFFIX = "_reply";
// ========== MQTT HTTP 接口路径常量 ========== // ========== MQTT HTTP 接口路径常量 ==========
/** /**
@@ -48,15 +53,12 @@ public final class IotMqttTopicUtils {
if (StrUtil.isBlank(method)) { if (StrUtil.isBlank(method)) {
return null; return null;
} }
// 1. 将点分隔符转换为斜杠 // 1. 将点分隔符转换为斜杠
String topicSuffix = method.replace('.', '/'); String topicSuffix = method.replace('.', '/');
// 2. 对于回复消息,添加 _reply 后缀 // 2. 对于回复消息,添加 _reply 后缀
if (isReply) { if (isReply) {
topicSuffix += "_reply"; topicSuffix += REPLY_TOPIC_SUFFIX;
} }
// 3. 构建完整主题 // 3. 构建完整主题
return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
} }