From bd18e730525671e1286a063d17604db483d41412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=B5=A9=E6=B5=A9?= <1036606149@qq.com> Date: Fri, 20 Sep 2024 23:33:00 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91IOT=20?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/iot/enums/ErrorCodeConstants.java | 5 + .../iot/enums/device/IotDeviceStatusEnum.java | 43 +++ .../admin/device/IotDeviceController.java | 104 +++++++ .../admin/device/vo/IotDevicePageReqVO.java | 98 +++++++ .../admin/device/vo/IotDeviceRespVO.java | 119 ++++++++ .../admin/device/vo/IotDeviceSaveReqVO.java | 22 ++ .../module/iot/convert/package-info.java | 1 + .../dal/dataobject/device/IotDeviceDO.java | 134 +++++++++ .../iot/dal/mysql/device/IotDeviceMapper.java | 56 ++++ .../iot/service/device/DeviceServiceImpl.java | 264 ++++++++++++++++++ .../iot/service/device/IotDeviceService.java | 60 ++++ .../mapper/device/IotDeviceMapper.xml | 12 + .../service/device/DeviceServiceImplTest.java | 219 +++++++++++++++ 13 files changed, 1137 insertions(+) create mode 100644 yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImplTest.java diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 790bbb40c..0a103b354 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -21,5 +21,10 @@ public interface ErrorCodeConstants { // ========== IoT 设备 1-050-003-000 ============ ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在"); + ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一"); + ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除"); + ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改"); + ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改"); + ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态"); } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java new file mode 100644 index 000000000..3d0f9fcc5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import lombok.Getter; + +/** + * IoT 设备状态枚举 + * 设备状态:0 - 未激活,1 - 在线,2 - 离线,3 - 已禁用 + */ +@Getter +public enum IotDeviceStatusEnum { + + INACTIVE(0, "未激活"), + ONLINE(1, "在线"), + OFFLINE(2, "离线"), + DISABLED(3, "已禁用"); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + IotDeviceStatusEnum(Integer status, String name) { + this.status = status; + this.name = name; + } + + public static IotDeviceStatusEnum fromStatus(Integer status) { + for (IotDeviceStatusEnum value : values()) { + if (value.getStatus().equals(status)) { + return value; + } + } + return null; + } + + public static boolean isValidStatus(Integer status) { + return fromStatus(status) != null; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java new file mode 100644 index 000000000..c809a9059 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备") +@RestController +@RequestMapping("/iot/device") +@Validated +public class IotDeviceController { + + @Resource + private IotDeviceService deviceService; + + @PostMapping("/create") + @Operation(summary = "创建IoT 设备") + @PreAuthorize("@ss.hasPermission('iot:device:create')") + public CommonResult createDevice(@Valid @RequestBody IotDeviceSaveReqVO createReqVO) { + return success(deviceService.createDevice(createReqVO)); + } + + @PutMapping("/update-status") + @Operation(summary = "更新IoT 设备状态") + @Parameter(name = "id", description = "编号", required = true) + @Parameter(name = "status", description = "状态", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult updateDeviceStatus(@RequestParam("id") Long id, + @RequestParam("status") Integer status) { + deviceService.updateDeviceStatus(id, status); + return success(true); + } + + @PutMapping("/update") + @Operation(summary = "更新IoT 设备") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult updateDevice(@Valid @RequestBody IotDeviceSaveReqVO updateReqVO) { + deviceService.updateDevice(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除IoT 设备") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device:delete')") + public CommonResult deleteDevice(@RequestParam("id") Long id) { + deviceService.deleteDevice(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得IoT 设备") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult getDevice(@RequestParam("id") Long id) { + IotDeviceDO device = deviceService.getDevice(id); + return success(BeanUtils.toBean(device, IotDeviceRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得IoT 设备分页") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getDevicePage(@Valid IotDevicePageReqVO pageReqVO) { + PageResult pageResult = deviceService.getDevicePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出IoT 设备 Excel") + @PreAuthorize("@ss.hasPermission('iot:device:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDeviceExcel(@Valid IotDevicePageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = deviceService.getDevicePage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "IoT 设备.xls", "数据", IotDeviceRespVO.class, + BeanUtils.toBean(list, IotDeviceRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java new file mode 100644 index 000000000..36a184871 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import java.math.BigDecimal; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 设备分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDevicePageReqVO extends PageParam { + + @Schema(description = "设备唯一标识符,全局唯一,用于识别设备") + private String deviceKey; + + @Schema(description = "设备名称,在产品内唯一,用于标识设备", example = "王五") + private String deviceName; + + @Schema(description = "产品 ID,关联 iot_product 表的 id", example = "26202") + private Long productId; + + @Schema(description = "产品 Key,关联 iot_product 表的 product_key") + private String productKey; + + @Schema(description = "设备类型:0 - 直连设备,1 - 网关子设备,2 - 网关设备", example = "1") + private Integer deviceType; + + @Schema(description = "设备备注名称,供用户自定义备注", example = "张三") + private String nickname; + + @Schema(description = "网关设备 ID,子设备需要关联的网关设备 ID", example = "16380") + private Long gatewayId; + + @Schema(description = "设备状态:0 - 未激活,1 - 在线,2 - 离线,3 - 已禁用", example = "1") + private Integer status; + + @Schema(description = "设备状态最后更新时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] statusLastUpdateTime; + + @Schema(description = "最后上线时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] lastOnlineTime; + + @Schema(description = "最后离线时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] lastOfflineTime; + + @Schema(description = "设备激活时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activeTime; + + @Schema(description = "设备的 IP 地址") + private String ip; + + @Schema(description = "设备的固件版本") + private String firmwareVersion; + + @Schema(description = "设备密钥,用于设备认证,需安全存储") + private String deviceSecret; + + @Schema(description = "MQTT 客户端 ID", example = "24602") + private String mqttClientId; + + @Schema(description = "MQTT 用户名", example = "芋艿") + private String mqttUsername; + + @Schema(description = "MQTT 密码") + private String mqttPassword; + + @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") + private String authType; + + @Schema(description = "设备位置的纬度,范围 -90.000000 ~ 90.000000") + private BigDecimal latitude; + + @Schema(description = "设备位置的经度,范围 -180.000000 ~ 180.000000") + private BigDecimal longitude; + + @Schema(description = "地区编码,符合国家地区编码标准,关联地区表", example = "16995") + private Integer areaId; + + @Schema(description = "设备详细地址") + private String address; + + @Schema(description = "设备序列号") + private String serialNumber; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java new file mode 100644 index 000000000..7cf592fc0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Response VO") +@Data +@ExcelIgnoreUnannotated +public class IotDeviceRespVO { + + @Schema(description = "设备 ID,主键,自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + private Long id; + + @Schema(description = "设备唯一标识符", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("设备唯一标识符") + private String deviceKey; + + @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @ExcelProperty("设备名称备") + private String deviceName; + + @Schema(description = "产品 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") + @ExcelProperty("产品 ID") + private Long productId; + + @Schema(description = "产品 Key", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("产品 Key") + private String productKey; + + @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("设备类型") + private Integer deviceType; + + @Schema(description = "设备备注名称", example = "张三") + @ExcelProperty("设备备注名称") + private String nickname; + + @Schema(description = "网关设备 ID", example = "16380") + @ExcelProperty("网关设备 ID") + private Long gatewayId; + + @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("设备状态") + private Integer status; + + @Schema(description = "设备状态最后更新时间") + @ExcelProperty("设备状态最后更新时间") + private LocalDateTime statusLastUpdateTime; + + @Schema(description = "最后上线时间") + @ExcelProperty("最后上线时间") + private LocalDateTime lastOnlineTime; + + @Schema(description = "最后离线时间") + @ExcelProperty("最后离线时间") + private LocalDateTime lastOfflineTime; + + @Schema(description = "设备激活时间") + @ExcelProperty("设备激活时间") + private LocalDateTime activeTime; + + @Schema(description = "设备的 IP 地址") + @ExcelProperty("设备的 IP 地址") + private String ip; + + @Schema(description = "设备的固件版本") + @ExcelProperty("设备的固件版本") + private String firmwareVersion; + + @Schema(description = "设备密钥,用于设备认证,需安全存储") + @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; + + @Schema(description = "设备位置的纬度,范围 -90.000000 ~ 90.000000") + @ExcelProperty("设备位置的纬度,范围 -90.000000 ~ 90.000000") + private BigDecimal latitude; + + @Schema(description = "设备位置的经度,范围 -180.000000 ~ 180.000000") + @ExcelProperty("设备位置的经度,范围 -180.000000 ~ 180.000000") + private BigDecimal longitude; + + @Schema(description = "地区编码,符合国家地区编码标准,关联地区表", example = "16995") + @ExcelProperty("地区编码,符合国家地区编码标准,关联地区表") + private Integer areaId; + + @Schema(description = "设备详细地址") + @ExcelProperty("设备详细地址") + private String address; + + @Schema(description = "设备序列号") + @ExcelProperty("设备序列号") + private String serialNumber; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java new file mode 100644 index 000000000..f52d8db92 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") +@Data +public class IotDeviceSaveReqVO { + + @Schema(description = "设备 ID,主键,自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + private Long id; + + @Schema(description = "设备名称,在产品内唯一,用于标识设备", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String deviceName; + + @Schema(description = "设备备注名称,供用户自定义备注", example = "张三") + private String nickname; + + @Schema(description = "产品 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") + private Long productId; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java new file mode 100644 index 000000000..c196c25c3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.iot.convert; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java new file mode 100644 index 000000000..138913f73 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * IoT 设备 DO + * + * @author 芋道源码 + */ +@TableName("iot_device") +@KeySequence("iot_device_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceDO extends BaseDO { + + /** + * 设备 ID,主键,自增 + */ + @TableId + private Long id; + /** + * 设备唯一标识符,全局唯一,用于识别设备 + */ + private String deviceKey; + /** + * 设备名称,在产品内唯一,用于标识设备 + */ + private String deviceName; + /** + * 产品 ID,关联 iot_product 表的 id + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 产品 Key,关联 iot_product 表的 product_key + * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备类型:0 - 直连设备,1 - 网关子设备,2 - 网关设备 + * 关联 {@link IotProductDO#getDeviceType()} + */ + private Integer deviceType; + /** + * 设备备注名称,供用户自定义备注 + */ + private String nickname; + /** + * 网关设备 ID,子设备需要关联的网关设备 ID + */ + private Long gatewayId; + /** + * 设备状态:0 - 未激活,1 - 在线,2 - 离线,3 - 已禁用 + * 关联 {@link cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum} + */ + private Integer status; + /** + * 设备状态最后更新时间 + */ + private LocalDateTime statusLastUpdateTime; + /** + * 最后上线时间 + */ + private LocalDateTime lastOnlineTime; + /** + * 最后离线时间 + */ + private LocalDateTime lastOfflineTime; + /** + * 设备激活时间 + */ + private LocalDateTime activeTime; + /** + * 设备的 IP 地址 + */ + private String ip; + /** + * 设备的固件版本 + */ + private String firmwareVersion; + /** + * 设备密钥,用于设备认证,需安全存储 + */ + private String deviceSecret; + /** + * MQTT 客户端 ID + */ + private String mqttClientId; + /** + * MQTT 用户名 + */ + private String mqttUsername; + /** + * MQTT 密码 + */ + private String mqttPassword; + /** + * 认证类型(如一机一密、动态注册) + */ + private String authType; + /** + * 设备位置的纬度,范围 -90.000000 ~ 90.000000 + */ + private BigDecimal latitude; + /** + * 设备位置的经度,范围 -180.000000 ~ 180.000000 + */ + private BigDecimal longitude; + /** + * 地区编码,符合国家地区编码标准,关联地区表 + */ + private Integer areaId; + /** + * 设备详细地址 + */ + private String address; + /** + * 设备序列号 + */ + private String serialNumber; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java new file mode 100644 index 000000000..fc7d0a71f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * IoT 设备 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceMapper extends BaseMapperX { + + default PageResult selectPage(IotDevicePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceDO::getDeviceKey, reqVO.getDeviceKey()) + .likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName()) + .eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId()) + .eqIfPresent(IotDeviceDO::getProductKey, reqVO.getProductKey()) + .eqIfPresent(IotDeviceDO::getDeviceType, reqVO.getDeviceType()) + .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname()) + .eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId()) + .eqIfPresent(IotDeviceDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotDeviceDO::getStatusLastUpdateTime, reqVO.getStatusLastUpdateTime()) + .betweenIfPresent(IotDeviceDO::getLastOnlineTime, reqVO.getLastOnlineTime()) + .betweenIfPresent(IotDeviceDO::getLastOfflineTime, reqVO.getLastOfflineTime()) + .betweenIfPresent(IotDeviceDO::getActiveTime, reqVO.getActiveTime()) + .eqIfPresent(IotDeviceDO::getIp, reqVO.getIp()) + .eqIfPresent(IotDeviceDO::getFirmwareVersion, reqVO.getFirmwareVersion()) + .eqIfPresent(IotDeviceDO::getDeviceSecret, reqVO.getDeviceSecret()) + .eqIfPresent(IotDeviceDO::getMqttClientId, reqVO.getMqttClientId()) + .likeIfPresent(IotDeviceDO::getMqttUsername, reqVO.getMqttUsername()) + .eqIfPresent(IotDeviceDO::getMqttPassword, reqVO.getMqttPassword()) + .eqIfPresent(IotDeviceDO::getAuthType, reqVO.getAuthType()) + .eqIfPresent(IotDeviceDO::getLatitude, reqVO.getLatitude()) + .eqIfPresent(IotDeviceDO::getLongitude, reqVO.getLongitude()) + .eqIfPresent(IotDeviceDO::getAreaId, reqVO.getAreaId()) + .eqIfPresent(IotDeviceDO::getAddress, reqVO.getAddress()) + .eqIfPresent(IotDeviceDO::getSerialNumber, reqVO.getSerialNumber()) + .betweenIfPresent(IotDeviceDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDeviceDO::getId)); + } + + default IotDeviceDO selectByProductKeyAndDeviceName(String productKey, String deviceName) { + return selectOne(IotDeviceDO::getProductKey, productKey, + IotDeviceDO::getDeviceName, deviceName); + } + + default long selectCountByGatewayId(Long id) { + return selectCount(IotDeviceDO::getGatewayId, id); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java new file mode 100644 index 000000000..b1e7ffbdd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java @@ -0,0 +1,264 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; +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.time.LocalDateTime; +import java.util.UUID; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class DeviceServiceImpl implements IotDeviceService { + + @Resource + private IotDeviceMapper deviceMapper; + @Resource + private IotProductMapper productMapper; + + /** + * 创建 IoT 设备 + * + * @param createReqVO 创建请求 VO + * @return 设备 ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDevice(IotDeviceSaveReqVO createReqVO) { + // 1. 转换 VO 为 DO + IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); + + // 2. 根据产品 ID 查询产品信息 + IotProductDO product = productMapper.selectById(createReqVO.getProductId()); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + device.setProductKey(product.getProductKey()); + device.setDeviceType(product.getDeviceType()); + + // 3. DeviceName 可以为空,当为空时,自动生成产品下的唯一标识符作为 DeviceName + if (StrUtil.isBlank(device.getDeviceName())) { + device.setDeviceName(generateUniqueDeviceName(createReqVO.getProductId())); + } + + // 4. 校验设备名称在同一产品下是否唯一 + validateDeviceNameUnique(device.getProductKey(), device.getDeviceName()); + + // 5. 生成并设置必要的字段 + device.setDeviceKey(generateUniqueDeviceKey()); + device.setDeviceSecret(generateDeviceSecret()); + device.setMqttClientId(generateMqttClientId()); + device.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey())); + device.setMqttPassword(generateMqttPassword()); + + // 6. 设置设备状态为未激活 + device.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus()); + device.setStatusLastUpdateTime(LocalDateTime.now()); + + // 7. 插入到数据库 + deviceMapper.insert(device); + + // 8. 返回生成的设备 ID + return device.getId(); + } + + /** + * 校验设备名称在同一产品下是否唯一 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + */ + private void validateDeviceNameUnique(String productKey, String deviceName) { + IotDeviceDO existingDevice = deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); + if (existingDevice != null) { + throw exception(DEVICE_NAME_EXISTS); + } + } + + /** + * 生成唯一的 deviceKey + * + * @return 生成的 deviceKey + */ + private String generateUniqueDeviceKey() { + return UUID.randomUUID().toString(); + } + + /** + * 生成 deviceSecret + * + * @return 生成的 deviceSecret + */ + private String generateDeviceSecret() { + // 32 位随机字符串 + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 生成 MQTT Client ID + * + * @return 生成的 MQTT Client ID + */ + private String generateMqttClientId() { + return UUID.randomUUID().toString(); + } + + /** + * 生成 MQTT Username + * + * @param deviceName 设备名称 + * @param productKey 产品 Key + * @return 生成的 MQTT Username + */ + private String generateMqttUsername(String deviceName, String productKey) { + return deviceName + "&" + productKey; + } + + /** + * 生成 MQTT Password + * + * @return 生成的 MQTT Password + */ + private String generateMqttPassword() { + // 在实际应用中,建议使用更安全的方法生成 MQTT Password,如加密或哈希 + return UUID.randomUUID().toString(); + } + + /** + * 生成唯一的 DeviceName + * + * @param productId 产品 ID + * @return 生成的唯一 DeviceName + */ + private String generateUniqueDeviceName(Long productId) { + // 实现逻辑以在产品下生成唯一的设备名称 + String deviceName; + String productKey = getProductKey(productId); + do { + // 20 位随机字符串 + deviceName = UUID.randomUUID().toString().replace("-", "").substring(0, 20); + } while (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null); + return deviceName; + } + + /** + * 获取产品 Key + * + * @param productId 产品 ID + * @return 产品 Key + */ + private String getProductKey(Long productId) { + IotProductDO product = productMapper.selectById(productId); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + return product.getProductKey(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDevice(IotDeviceSaveReqVO updateReqVO) { + // 校验存在 + IotDeviceDO existingDevice = validateDeviceExists(updateReqVO.getId()); + + // 设备名称 和 产品 ID 不能修改 + if (updateReqVO.getDeviceName() != null && !updateReqVO.getDeviceName().equals(existingDevice.getDeviceName())) { + throw exception(DEVICE_NAME_CANNOT_BE_MODIFIED); + } + if (updateReqVO.getProductId() != null && !updateReqVO.getProductId().equals(existingDevice.getProductId())) { + throw exception(DEVICE_PRODUCT_CANNOT_BE_MODIFIED); + } + + // 更新 DO 对象 + IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); + + // 更新到数据库 + deviceMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDevice(Long id) { + // 校验存在 + IotDeviceDO iotDeviceDO = validateDeviceExists(id); + + // 如果是网关设备,检查是否有子设备 + if (iotDeviceDO.getGatewayId() != null) { + long childCount = deviceMapper.selectCountByGatewayId(id); + if (childCount > 0) { + throw exception(DEVICE_HAS_CHILDREN); + } + } + + // 删除设备 + deviceMapper.deleteById(id); + } + + /** + * 校验设备是否存在 + * + * @param id 设备 ID + * @return 设备对象 + */ + private IotDeviceDO validateDeviceExists(Long id) { + IotDeviceDO iotDeviceDO = deviceMapper.selectById(id); + if (iotDeviceDO == null) { + throw exception(DEVICE_NOT_EXISTS); + } + return iotDeviceDO; + } + + @Override + public IotDeviceDO getDevice(Long id) { + IotDeviceDO device = deviceMapper.selectById(id); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS); + } + return device; + } + + @Override + public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { + return deviceMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDeviceStatus(Long id, Integer status) { + // 校验存在 + validateDeviceExists(id); + + // 校验状态是否合法 + if (!IotDeviceStatusEnum.isValidStatus(status)) { + throw exception(DEVICE_INVALID_DEVICE_STATUS); + } + + // 更新状态和更新时间 + IotDeviceDO updateObj = new IotDeviceDO() + .setId(id) + .setStatus(status) + .setStatusLastUpdateTime(LocalDateTime.now()); + deviceMapper.updateById(updateObj); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java new file mode 100644 index 000000000..2802806de --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import jakarta.validation.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +/** + * IoT 设备 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceService { + + /** + * 创建IoT 设备 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDevice(@Valid IotDeviceSaveReqVO createReqVO); + + /** + * 更新IoT 设备 + * + * @param updateReqVO 更新信息 + */ + void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO); + + /** + * 删除IoT 设备 + * + * @param id 编号 + */ + void deleteDevice(Long id); + + /** + * 获得IoT 设备 + * + * @param id 编号 + * @return IoT 设备 + */ + IotDeviceDO getDevice(Long id); + + /** + * 获得IoT 设备分页 + * + * @param pageReqVO 分页查询 + * @return IoT 设备分页 + */ + PageResult getDevicePage(IotDevicePageReqVO pageReqVO); + + /** + * 更新IoT 设备状态 + * + * @param id 编号 + * @param status 状态 + */ + void updateDeviceStatus(Long id, Integer status); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml new file mode 100644 index 000000000..039dbd895 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImplTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImplTest.java new file mode 100644 index 000000000..a456589cb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImplTest.java @@ -0,0 +1,219 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import org.springframework.context.annotation.Import; + +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(DeviceServiceImpl.class) +public class DeviceServiceImplTest extends BaseDbUnitTest { + + @Resource + private DeviceServiceImpl deviceService; + + @Resource + private IotDeviceMapper deviceMapper; + + @Test + public void testCreateDevice_success() { + // 准备参数 + IotDeviceSaveReqVO createReqVO = randomPojo(IotDeviceSaveReqVO.class).setId(null); + + // 调用 + Long deviceId = deviceService.createDevice(createReqVO); + // 断言 + assertNotNull(deviceId); + // 校验记录的属性是否正确 + IotDeviceDO device = deviceMapper.selectById(deviceId); + assertPojoEquals(createReqVO, device, "id"); + } + + @Test + public void testUpdateDevice_success() { + // mock 数据 + IotDeviceDO dbDevice = randomPojo(IotDeviceDO.class); + deviceMapper.insert(dbDevice);// @Sql: 先插入出一条存在的数据 + // 准备参数 + IotDeviceSaveReqVO updateReqVO = randomPojo(IotDeviceSaveReqVO.class, o -> { + o.setId(dbDevice.getId()); // 设置更新的 ID + }); + + // 调用 + deviceService.updateDevice(updateReqVO); + // 校验是否更新正确 + IotDeviceDO device = deviceMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, device); + } + + @Test + public void testUpdateDevice_notExists() { + // 准备参数 + IotDeviceSaveReqVO updateReqVO = randomPojo(IotDeviceSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> deviceService.updateDevice(updateReqVO), DEVICE_NOT_EXISTS); + } + + @Test + public void testDeleteDevice_success() { + // mock 数据 + IotDeviceDO dbDevice = randomPojo(IotDeviceDO.class); + deviceMapper.insert(dbDevice);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDevice.getId(); + + // 调用 + deviceService.deleteDevice(id); + // 校验数据不存在了 + assertNull(deviceMapper.selectById(id)); + } + + @Test + public void testDeleteDevice_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> deviceService.deleteDevice(id), DEVICE_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetDevicePage() { + // mock 数据 + IotDeviceDO dbDevice = randomPojo(IotDeviceDO.class, o -> { // 等会查询到 + o.setDeviceKey(null); + o.setDeviceName(null); + o.setProductId(null); + o.setProductKey(null); + o.setDeviceType(null); + o.setNickname(null); + o.setGatewayId(null); + o.setStatus(null); + o.setStatusLastUpdateTime(null); + o.setLastOnlineTime(null); + o.setLastOfflineTime(null); + o.setActiveTime(null); + o.setIp(null); + o.setFirmwareVersion(null); + o.setDeviceSecret(null); + o.setMqttClientId(null); + o.setMqttUsername(null); + o.setMqttPassword(null); + o.setAuthType(null); + o.setLatitude(null); + o.setLongitude(null); + o.setAreaId(null); + o.setAddress(null); + o.setSerialNumber(null); + o.setCreateTime(null); + }); + deviceMapper.insert(dbDevice); + // 测试 deviceKey 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setDeviceKey(null))); + // 测试 deviceName 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setDeviceName(null))); + // 测试 productId 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setProductId(null))); + // 测试 productKey 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setProductKey(null))); + // 测试 deviceType 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setDeviceType(null))); + // 测试 nickname 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setNickname(null))); + // 测试 gatewayId 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setGatewayId(null))); + // 测试 status 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setStatus(null))); + // 测试 statusLastUpdateTime 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setStatusLastUpdateTime(null))); + // 测试 lastOnlineTime 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setLastOnlineTime(null))); + // 测试 lastOfflineTime 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setLastOfflineTime(null))); + // 测试 activeTime 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setActiveTime(null))); + // 测试 ip 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setIp(null))); + // 测试 firmwareVersion 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setFirmwareVersion(null))); + // 测试 deviceSecret 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setDeviceSecret(null))); + // 测试 mqttClientId 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setMqttClientId(null))); + // 测试 mqttUsername 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setMqttUsername(null))); + // 测试 mqttPassword 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setMqttPassword(null))); + // 测试 authType 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setAuthType(null))); + // 测试 latitude 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setLatitude(null))); + // 测试 longitude 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setLongitude(null))); + // 测试 areaId 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setAreaId(null))); + // 测试 address 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setAddress(null))); + // 测试 serialNumber 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setSerialNumber(null))); + // 测试 createTime 不匹配 + deviceMapper.insert(cloneIgnoreId(dbDevice, o -> o.setCreateTime(null))); + // 准备参数 + IotDevicePageReqVO reqVO = new IotDevicePageReqVO(); + reqVO.setDeviceKey(null); + reqVO.setDeviceName(null); + reqVO.setProductId(null); + reqVO.setProductKey(null); + reqVO.setDeviceType(null); + reqVO.setNickname(null); + reqVO.setGatewayId(null); + reqVO.setStatus(null); + reqVO.setStatusLastUpdateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setLastOnlineTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setLastOfflineTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setActiveTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setIp(null); + reqVO.setFirmwareVersion(null); + reqVO.setDeviceSecret(null); + reqVO.setMqttClientId(null); + reqVO.setMqttUsername(null); + reqVO.setMqttPassword(null); + reqVO.setAuthType(null); + reqVO.setLatitude(null); + reqVO.setLongitude(null); + reqVO.setAreaId(null); + reqVO.setAddress(null); + reqVO.setSerialNumber(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = deviceService.getDevicePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDevice, pageResult.getList().get(0)); + } + +} \ No newline at end of file