diff --git a/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/enums/ErrorCodeConstants.java b/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/enums/ErrorCodeConstants.java index c9701cd..78bce04 100644 --- a/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/enums/ErrorCodeConstants.java +++ b/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/enums/ErrorCodeConstants.java @@ -11,6 +11,9 @@ public interface ErrorCodeConstants { // ========== 管理客户页面 (1-030-100-000) ========== ErrorCode MANAGEMENT_NOT_EXISTS = new ErrorCode(1_030_100_000, "客户不存在"); + ErrorCode MANAGEMENT_CODE_NOT_EXISTS = new ErrorCode(1_030_100_000, "客户编码不能为空"); + ErrorCode MANAGEMENT_CUSTOMER_CODE_DUPLICATE = new ErrorCode(1_002_000_001, "客户编码已存在,请勿重复"); + } diff --git a/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/service/management/ManagementServiceImpl.java b/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/service/management/ManagementServiceImpl.java index a28d2a1..560c934 100644 --- a/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/service/management/ManagementServiceImpl.java +++ b/yudao-module-customer/src/main/java/cn/iocoder/yudao/module/cus/service/management/ManagementServiceImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.cus.service.management; +import cn.hutool.core.util.StrUtil; import org.springframework.stereotype.Service; import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; @@ -12,8 +13,8 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper; -import static cn.iocoder.yudao.module.cus.enums.ErrorCodeConstants.MANAGEMENT_NOT_EXISTS; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.cus.enums.ErrorCodeConstants.*; /** * 客户管理 Service 实现类 @@ -29,6 +30,8 @@ public class ManagementServiceImpl implements ManagementService { @Override public Long createManagement(ManagementSaveReqVO createReqVO) { + //customerCode 唯一校验 + validateCustomerCodeUnique(createReqVO.getCustomerCode(), null); // 插入 ManagementDO management = BeanUtils.toBean(createReqVO, ManagementDO.class); managementMapper.insert(management); @@ -36,6 +39,23 @@ public class ManagementServiceImpl implements ManagementService { // 返回 return management.getId(); } + private void validateCustomerCodeUnique(String customerCode, Long id) { + if (StrUtil.isBlank(customerCode)) { + throw exception(MANAGEMENT_CODE_NOT_EXISTS); + } + + ManagementDO exist = managementMapper.selectOne( + ManagementDO::getCustomerCode, customerCode + ); + if (exist == null) { + return; + } + + // update 场景排除自己(create 时 id = null) + if (id == null || !Objects.equals(exist.getId(), id)) { + throw exception(MANAGEMENT_CUSTOMER_CODE_DUPLICATE); + } + } @Override public void updateManagement(ManagementSaveReqVO updateReqVO) { diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index 02644cb..04f402f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -101,6 +101,12 @@ spring-boot-starter-amqp true + + cn.iocoder.boot + yudao-module-customer + 2026.01-jdk8-SNAPSHOT + compile + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java index c4e57f7..2269b99 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java @@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.infra.service.job.JobService; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; //import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; //import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO; @@ -120,10 +122,9 @@ public class DeviceController { ExcelUtils.write(response, "物联设备.xls", "数据", DeviceRespVO.class,list); } @GetMapping("/deviceList") -// @PreAuthorize("@ss.hasPermission('iot:device:query')") + @PreAuthorize("@ss.hasPermission('iot:device:query')") public CommonResult> deviceList(@Valid DevicePageReqVO pageReqVO) { - pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); - List list = deviceService.getDevicePage(pageReqVO).getList(); + List list = deviceService.deviceList(pageReqVO); return success(list); } @@ -240,11 +241,11 @@ public class DeviceController { @GetMapping("/getDeviceOperationalStatus") - @Operation(summary = "获取首页设备运行状态") + @Operation(summary = "获取设备运行状态") // @PreAuthorize("@ss.hasPermission('iot:device:query')") @Parameter(name = "orgId", description = "产线组织Id") - public CommonResult getDeviceOperationalStatus(@RequestParam(name = "orgId",required = false) Long orgId) throws JsonProcessingException { - DeviceOperationStatusRespVO deviceOperationalStatus=deviceService.getDeviceOperationalStatus(); + public CommonResult getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime) throws JsonProcessingException { + DeviceOperationStatusRespVO deviceOperationalStatus=deviceService.getDeviceOperationalStatus(startTime,endTime); return success(deviceOperationalStatus); } @@ -350,4 +351,30 @@ public class DeviceController { return success(true); } + + @GetMapping("/device-run-status-stats") + @Operation(summary = "查询设备总数及各运行状态数量") + @PreAuthorize("@ss.hasPermission('iot:org-node:query')") + public CommonResult getDeviceRunStatusStats( + @RequestParam(value = "customerId", required = false) Long customerId, + @RequestParam(value = "orgNodeId", required = false) Long orgNodeId) { + return success(deviceService.getDeviceRunStatusStats(customerId, orgNodeId)); + } + + @GetMapping("/run-status-stats-by-customer") + @Operation(summary = "按客户统计设备总数及运行状态数") + @Parameter(name = "customerId", description = "客户ID", required = true, example = "5") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult getRunStatusStatsByCustomer(@RequestParam("customerId") Long customerId) { + return success(deviceService.getRunStatusStatsByCustomer(customerId)); + } + + @GetMapping("/status-count-by-customer") + @Operation(summary = "按客户统计设备状态数量") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getStatusCountByCustomer() { + return success(deviceService.getStatusCountByCustomer()); + } + + } \ 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/dto/DeviceLatestRuleDTO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/dto/DeviceLatestRuleDTO.java new file mode 100644 index 0000000..b227457 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/dto/DeviceLatestRuleDTO.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.dto; + +import lombok.Data; + +@Data +public class DeviceLatestRuleDTO { + private Long deviceId; + private String rule; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/CustomerDeviceStatusStatsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/CustomerDeviceStatusStatsRespVO.java new file mode 100644 index 0000000..ad4bb77 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/CustomerDeviceStatusStatsRespVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class CustomerDeviceStatusStatsRespVO { + private Long customerId; + private String customerName; + private Long totalDeviceCount; + private Long offlineCount; + private Long runningCount; + private Long standbyCount; + private Long faultStandbyCount; + private Long alarmRunningCount; + + /** + * 经度 + */ + private BigDecimal longitude; + /** + * 纬度 + */ + private BigDecimal latitude; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceOperationStatusRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceOperationStatusRespVO.java index da93533..a9798f9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceOperationStatusRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceOperationStatusRespVO.java @@ -31,6 +31,9 @@ public class DeviceOperationStatusRespVO { @Schema(description = "故障率") private String faultRate; + @Schema(description = "报告统计数") + private Long warningRecordCount; + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRespVO.java index 4af131c..1690925 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRespVO.java @@ -127,4 +127,10 @@ public class DeviceRespVO { @Schema(description = "客户组织节点Id") private Long orgNodeId; + + @Schema(description = "客户名称") + private String customerName; + + @Schema(description = "组织名称") + private String orgNodeName; } \ 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/DeviceRunStatusStatsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRunStatusStatsRespVO.java new file mode 100644 index 0000000..c91a49a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceRunStatusStatsRespVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import lombok.Data; + +@Data +public class DeviceRunStatusStatsRespVO { + + private Long totalDeviceCount; // 设备总数 + + private Long offlineCount; // 0 离线 + private Long runningCount; // 1 运行 + private Long standbyCount; // 2 待机中 + private Long faultStandbyCount; // 3 故障中 + private Long alarmRunningCount; // 4 报警中 +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceSimpleRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceSimpleRespVO.java new file mode 100644 index 0000000..3945fa8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/DeviceSimpleRespVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 设备简要信息 Response VO") +@Data +public class DeviceSimpleRespVO { + + @Schema(description = "设备ID", example = "170") + private Long id; + + @Schema(description = "设备编号", example = "DEV-001") + private String deviceCode; + + @Schema(description = "设备名称", example = "注塑机-01") + private String deviceName; + + @Schema(description = "状态", example = "2") + private String status; + + @Schema(description = "客户ID", example = "5") + private Long customerId; + + @Schema(description = "组织节点ID", example = "12") + private Long orgNodeId; + + @Schema(description = "通讯协议", example = "") + private String protocol; + + @Schema(description = "运行状态") + private String operatingStatus; + + @Schema(description = "是否启用") + private Boolean isEnable; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/DeviceWarinningRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/DeviceWarinningRecordController.java index 6dc94c2..83f5c97 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/DeviceWarinningRecordController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/DeviceWarinningRecordController.java @@ -6,9 +6,7 @@ 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.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; import cn.iocoder.yudao.module.iot.service.devicewarinningrecord.DeviceWarinningRecordService; import io.swagger.v3.oas.annotations.Operation; @@ -73,9 +71,9 @@ public class DeviceWarinningRecordController { @GetMapping("/page") @Operation(summary = "获得告警记录分页") @PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')") - public CommonResult> getDeviceWarinningRecordPage(@Valid DeviceWarinningRecordPageReqVO pageReqVO) { - PageResult pageResult = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, DeviceWarinningRecordRespVO.class)); + public CommonResult> getDeviceWarinningRecordPage(@Valid DeviceWarinningRecordPageReqVO pageReqVO) { + PageResult pageResult = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO); + return success(pageResult); } @GetMapping("/export-excel") @@ -85,22 +83,18 @@ public class DeviceWarinningRecordController { public void exportDeviceWarinningRecordExcel(@Valid DeviceWarinningRecordPageReqVO pageReqVO, HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); - List list = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO).getList(); + List list = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO).getList(); // 导出 Excel - ExcelUtils.write(response, "告警记录.xls", "数据", DeviceWarinningRecordRespVO.class, - BeanUtils.toBean(list, DeviceWarinningRecordRespVO.class)); + ExcelUtils.write(response, "告警记录.xls", "数据", DeviceWarinningRecordPageRespVO.class, + list); } @GetMapping("/getList") @Operation(summary = "获得告警记录列表") - @Parameter(name = "deviceId", description = "设备Id", required = true, example = "1024") - @Parameter(name = "orgId", description = "产线组织Id", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')") - public CommonResult> getList(@RequestParam(name = "deviceId" ,required = false) Long id, - @RequestParam(name = "orgId" ,required = false) Long orgId) { - List deviceWarinningRecord = deviceWarinningRecordService.getList(id); - return success(deviceWarinningRecord); + public CommonResult> getList(@Valid DeviceWarningListReqVO reqVO) { + return success(deviceWarinningRecordService.getList(reqVO)); } @@ -113,4 +107,20 @@ public class DeviceWarinningRecordController { return success(hourCounts); } + @GetMapping("/count") + @Operation(summary = "告警数量统计") + @PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')") + public CommonResult count(@Valid DeviceWarningCountReqVO reqVO) { + return success(deviceWarinningRecordService.getWarningCount(reqVO)); + } + + + + @GetMapping("/trend") + @Operation(summary = "告警趋势(今日/本周/本月)") + @PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')") + public CommonResult trend(@Valid DeviceWarningTrendReqVO reqVO) { + return CommonResult.success(deviceWarinningRecordService.getWarningTrend(reqVO)); + } + } \ 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/devicewarinningrecord/vo/DeviceWarinningRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageReqVO.java index 9f91761..2f3b184 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageReqVO.java @@ -1,12 +1,14 @@ package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; +import javax.validation.constraints.NotNull; import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @@ -32,9 +34,11 @@ public class DeviceWarinningRecordPageReqVO extends PageParam { @Schema(description = "地址值") private String addressValue; - @Schema(description = "创建时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] createTime; + @NotNull(message = "开始时间不能为空") + private String startTime; + + @NotNull(message = "结束时间不能为空") + private String endTime; @Schema(description = "点位规则Id", example = "25946") private Long ruleId; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageRespVO.java new file mode 100644 index 0000000..8dcd50b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarinningRecordPageRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + + +import lombok.Data; + +@Data +public class DeviceWarinningRecordPageRespVO { + + private Long id; + private Long deviceId; + private String alarmLevel; + private String alarmContent; + private String createTime; + private Integer deleted; + + private String deviceName; + private String deviceCode; + private String customerCode; + private String customerName; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountReqVO.java new file mode 100644 index 0000000..ac5c444 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +// DeviceWarningCountReqVO.java +@Data +public class DeviceWarningCountReqVO { + + @NotNull(message = "开始时间不能为空") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @NotNull(message = "结束时间不能为空") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountRespVO.java new file mode 100644 index 0000000..d259e7b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningCountRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import lombok.Data; + +// DeviceWarningCountRespVO.java +@Data +public class DeviceWarningCountRespVO { + + private Long totalCount; + + // alarmLevel = 1 + private Long normalCount; + + // alarmLevel = 2 + private Long tipCount; + + // alarmLevel = 0 + private Long seriousCount; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListReqVO.java new file mode 100644 index 0000000..f3cb848 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Data +public class DeviceWarningListReqVO { + + private Long deviceId; // 可选 + private Long orgId; // 可选(先保留) + + @NotNull(message = "开始时间不能为空") + private LocalDateTime startTime; + + @NotNull(message = "结束时间不能为空") + private LocalDateTime endTime; + + private String alarmLevel; // 可选 +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListRespVO.java new file mode 100644 index 0000000..c4e3195 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningListRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class DeviceWarningListRespVO { + private Long id; + private Long deviceId; + private Long modelId; + private String rule; + private String alarmLevel; + private String addressValue; + private Long ruleId; + private String deviceName; + private String modelName; + private String ruleName; + private LocalDateTime createTime; + + // 新增 + private String customerName; +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningTrendReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningTrendReqVO.java new file mode 100644 index 0000000..0869ea1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningTrendReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Data +@Schema(description = "管理后台 - 设备告警趋势 Request VO") +public class DeviceWarningTrendReqVO { + + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-01 00:00:00") + private LocalDateTime startTime; + + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-31 23:59:59") + private LocalDateTime endTime; +} \ 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/devicewarinningrecord/vo/DeviceWarningTrendRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningTrendRespVO.java new file mode 100644 index 0000000..fc66312 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicewarinningrecord/vo/DeviceWarningTrendRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class DeviceWarningTrendRespVO { + + @Schema(description = "X轴时间点") + private List timePoints; + + @Schema(description = "Y轴告警总数") + private List counts; +} \ 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/orgnode/OrgNodeController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/OrgNodeController.java index 2fee65c..e9329b7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/OrgNodeController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/OrgNodeController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.orgnode; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; @@ -107,4 +108,12 @@ public class OrgNodeController { return CommonResult.success(orgNodeService.getOrgTree(customerId)); } + @GetMapping("/device-list-by-node") + @Operation(summary = "按树节点获取设备列表") + public CommonResult> getDeviceListByNode(@RequestParam("nodeId") Long nodeId, + @RequestParam("nodeType") Integer nodeType) { + return success(orgNodeService.getDeviceListByNode(nodeId, nodeType)); + } + + } \ 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/orgnode/vo/OrgNodeSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeSaveReqVO.java index 5119b72..1086541 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeSaveReqVO.java @@ -29,7 +29,7 @@ public class OrgNodeSaveReqVO { private String name; @Schema(description = "排序字段,数值越小越靠前", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "排序字段,数值越小越靠前不能为空") +// @NotNull(message = "排序字段,数值越小越靠前不能为空") private Integer sort; } \ 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/orgnode/vo/OrgNodeTreeRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeTreeRespVO.java index eb727d1..de30e5a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeTreeRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/orgnode/vo/OrgNodeTreeRespVO.java @@ -8,7 +8,14 @@ import java.util.List; public class OrgNodeTreeRespVO { private Long id; private Long parentId; - private Integer nodeType; // 1客户 2车间 3产线 + private Integer nodeType; // 1客户 2车间 3产线 4设备 private String name; private List children; + private String operateStatus; + + // 全局唯一节点标识 + private String nodeKey; // C_5 / O_12 / D_170 + private String parentKey; // 0 / C_5 / O_12 + + private String deviceCode; //设备编码 } \ 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/devicewarinningrecord/WarningTrendPointRespDTO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/devicewarinningrecord/WarningTrendPointRespDTO.java new file mode 100644 index 0000000..e83a2d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/devicewarinningrecord/WarningTrendPointRespDTO.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord; + + +import lombok.Data; + +@Data +public class WarningTrendPointRespDTO { + private String timeKey; + private Long count; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java index f467c9f..6fe5f6e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java @@ -127,4 +127,21 @@ public interface DeviceMapper extends BaseMapperX { Integer getTotalDeviceCount(); List getAllDeviceIds(); + + + List selectByTenantCustomerAndOrgNodeIds(@Param("tenantId") Long tenantId, + @Param("customerId") Long customerId, + @Param("orgNodeIds") List orgNodeIds); + + + List selectByCustomerAndOrgNodeIds(@Param("customerId") Long customerId, + @Param("orgNodeIds") List orgNodeIds); + + List selectByOrgNodeIds(@Param("orgNodeIds") List orgNodeIds); + + List selectByCustomerId(@Param("customerId") Long customerId); + + boolean existsByOrgNodeId(@Param("orgNodeId") Long orgNodeId); + + } \ 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/devicepointrules/DevicePointRulesMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicepointrules/DevicePointRulesMapper.java index cf2086f..3e512d4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicepointrules/DevicePointRulesMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicepointrules/DevicePointRulesMapper.java @@ -17,7 +17,7 @@ public interface DevicePointRulesMapper extends BaseMapperX default PageResult selectPage(DevicePointRulesPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(DevicePointRulesDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(DevicePointRulesDO::getIdentifier, reqVO.getIdentifier()) .likeIfPresent(DevicePointRulesDO::getFieldName, reqVO.getFieldName()) .eqIfPresent(DevicePointRulesDO::getFieldRule, reqVO.getFieldRule()) .eqIfPresent(DevicePointRulesDO::getDefaultValue, reqVO.getDefaultValue()) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicewarinningrecord/DeviceWarinningRecordMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicewarinningrecord/DeviceWarinningRecordMapper.java index 8227665..49b46ad 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicewarinningrecord/DeviceWarinningRecordMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/devicewarinningrecord/DeviceWarinningRecordMapper.java @@ -25,7 +25,7 @@ public interface DeviceWarinningRecordMapper extends BaseMapperX { List selectListByTenantAndCustomer(@Param("tenantId") Long tenantId, @Param("customerId") Long customerId); + + default List selectListByCustomer(Long customerId) { + return selectList(new LambdaQueryWrapperX() + .eq(OrgNodeDO::getCustomerId, customerId) + .eq(OrgNodeDO::getDeleted, false) + .orderByAsc(OrgNodeDO::getSort) + .orderByAsc(OrgNodeDO::getId)); + } + + List selectListByCustomerByNodeId(@Param("nodeId") Long nodeId); + + boolean existsByParentId(@Param("parentId") Long parentId); + + boolean existsByOrgNodeId(@Param("orgNodeId") Long orgNodeId); + + } \ 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/tdengine/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMapper.java new file mode 100644 index 0000000..3180fe5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMapper.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.iocoder.yudao.module.iot.controller.admin.device.dto.DeviceLatestRuleDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarningCountRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDeviceMapper { + + + void alterProductPropertySTableDropField(@Param("productId") Long productId, + @Param("field") TDengineTableField field); + Long countWarningRecordByTime(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + List selectWarningRecordList(@Param("deviceId") Long deviceId, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("alarmLevel") String alarmLevel); + // IotDeviceMapper.java (TDengine mapper) + DeviceWarningCountRespVO selectWarningCount(@Param("startTime") String startTime, + @Param("endTime") String endTime); + + + List selectWarningTrendByHour(@Param("startTime") String startTime, + @Param("endTime") String endTime); + + List selectWarningTrendByDay(@Param("startTime") String startTime, + @Param("endTime") String endTime); + + + List selectLatestRuleByDeviceIds(@Param("deviceIds") List deviceIds); + + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java deleted file mode 100644 index 484589d..0000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java +++ /dev/null @@ -1,81 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.tdengine; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; - -import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; -import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Mapper -@TDengineDS -@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 -public interface IotDevicePropertyMapper { - - List getProductPropertySTableFieldList(@Param("productId") Long productId); - - void createProductPropertySTable(@Param("productId") Long productId, - @Param("fields") List fields); - - @SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性 - default void alterProductPropertySTable(Long productId, - List oldFields, - List newFields) { - oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(), - TDengineTableField.FIELD_TS, "report_time", "device_id")); - List addFields = newFields.stream().filter( // 新增的字段 - newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField()))) - .collect(Collectors.toList()); - List dropFields = oldFields.stream().filter( // 删除的字段 - oldField -> newFields.stream().noneMatch(n -> n.getField().equals(oldField.getField()))) - .collect(Collectors.toList()); - List modifyTypeFields = new ArrayList<>(); // 变更类型的字段 - List modifyLengthFields = new ArrayList<>(); // 变更长度的字段 - newFields.forEach(newField -> { - TDengineTableField oldField = CollUtil.findOne(oldFields, field -> field.getField().equals(newField.getField())); - if (oldField == null) { - return; - } - if (ObjectUtil.notEqual(oldField.getType(), newField.getType())) { - modifyTypeFields.add(newField); - return; - } - if (newField.getLength() != null) { - if (newField.getLength() > oldField.getLength()) { - modifyLengthFields.add(newField); - } else if (newField.getLength() < oldField.getLength()) { - // 特殊:TDengine 长度修改时,只允许变长,所以此时认为是修改类型 - modifyTypeFields.add(newField); - } - } - }); - - // 执行 - addFields.forEach(field -> alterProductPropertySTableAddField(productId, field)); - dropFields.forEach(field -> alterProductPropertySTableDropField(productId, field)); - modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productId, field)); - modifyTypeFields.forEach(field -> { - alterProductPropertySTableDropField(productId, field); - alterProductPropertySTableAddField(productId, field); - }); - } - - void alterProductPropertySTableAddField(@Param("productId") Long productId, - @Param("field") TDengineTableField field); - - void alterProductPropertySTableModifyField(@Param("productId") Long productId, - @Param("field") TDengineTableField field); - - void alterProductPropertySTableDropField(@Param("productId") Long productId, - @Param("field") TDengineTableField field); - - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 590b9c6..b2e16fc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -9,34 +9,11 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; */ public interface ErrorCodeConstants { - // ========== 产品相关 1-050-001-000 ============ - ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_050_001_000, "产品不存在"); - ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); - ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除"); - ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型"); - ErrorCode PRODUCT_DELETE_FAIL_HAS_DEVICE = new ErrorCode(1_050_001_004, "产品下存在设备,不允许删除"); - - // ========== 产品物模型 1-050-002-000 ============ - ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); - ErrorCode THING_MODEL_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在"); - ErrorCode THING_MODEL_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。"); - ErrorCode THING_MODEL_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。"); - ErrorCode THING_MODEL_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效"); // ========== 设备 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_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除"); ErrorCode GATEWAY_NOT_EXISTS = new ErrorCode(1_003_000_000, "网关不存在"); - ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在"); - ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在"); - ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备"); - ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); - ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); - ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); - ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关"); - ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑"); - ErrorCode DEVICE_CONTACT_MODEL_NOT_EXISTS = new ErrorCode(1_050_003_011, "查询不到该点位"); + ErrorCode DEVICE_CONTACT_MODEL_NOT_EXISTS = new ErrorCode(1_050_003_011, "查询不到该点位"); ErrorCode DEVICE_MODEL_POINT_CODE_EXISTS = new ErrorCode(1_003_000_005, "采集设备采集点位编码已存在"); ErrorCode DEVICE_MODEL_ATTRIBUTE_POTIN_CODE_EXISTS = new ErrorCode(1_003_000_005, "采集设备模型点位编码已存在"); ErrorCode DEVICE_MODEL_ATTRIBUTE_NOT_EXISTS = new ErrorCode(1_003_000_005, "采集设备模型点位不存在"); @@ -57,75 +34,14 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_ID_MODEL_NOT_EXISTS = new ErrorCode(1_003_000_003, "该设备模型ID不能为空"); ErrorCode DEVICE_DOES_NOT_EXIST= new ErrorCode(1_003_000_010, "该采集设备不存在"); ErrorCode DEVICE_MQTT_TOPIC_EXIST = new ErrorCode(1_003_000_000, "设备MQTT主题不存在。"); + ErrorCode DEVICE_MODEL_CODE_EXISTS = new ErrorCode(1_003_000_002, "采集设备模型编码已存在"); + ErrorCode DEVICE_WARNING_TIME_REQUIRED = new ErrorCode(1_003_000_002, "开始时间和结束时间不能为空"); + ErrorCode ORG_NODE_CUSTOMER_ID_REQUIRED = new ErrorCode(1_003_000_002, "客户节点不能为空"); + ErrorCode DEVICE_WARNING_TIME_RANGE_INVALID = new ErrorCode(1_003_000_003, "开始时间不能大于结束时间"); + ErrorCode ORG_NODE_DELETE_HAS_CHILDREN = new ErrorCode(1_003_001_001, "当前节点下存在子节点,请先删除子节点"); + ErrorCode ORG_NODE_DELETE_HAS_DEVICES = new ErrorCode(1_003_001_002, "当前节点下存在设备,请先解绑或删除设备"); - // 拓扑管理相关错误码 1-050-003-100 - ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效"); - ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); - ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败"); - ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关"); - // 设备注册相关错误码 1-050-003-200 - ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); - ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型"); - ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能"); - ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败"); - ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册"); - - // ========== 产品分类 1-050-004-000 ========== - ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); - - // ========== 设备分组 1-050-005-000 ========== - ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); - ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); - - // ========== 设备 Modbus 配置 1-050-006-000 ========== - ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); - ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); - - // ========== 设备 Modbus 点位 1-050-007-000 ========== - ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); - ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); - - // ========== OTA 固件相关 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, "产品版本号重复"); - - // ========== OTA 升级任务相关 1-050-008-100 ========== - - ErrorCode OTA_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在"); - ErrorCode OTA_TASK_CREATE_FAIL_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "创建 OTA 任务失败,原因:任务名称重复"); - ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_FIRMWARE_EXISTS = new ErrorCode(1_050_008_102, - "创建 OTA 任务失败,原因:设备({})已经是该固件版本"); - ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_OTA_IN_PROCESS = new ErrorCode(1_050_008_102, - "创建 OTA 任务失败,原因:设备({})已经在升级中..."); - ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_EMPTY = new ErrorCode(1_050_008_103, "创建 OTA 任务失败,原因:没有可升级的设备"); - ErrorCode OTA_TASK_CANCEL_FAIL_STATUS_END = new ErrorCode(1_050_008_104, "取消 OTA 任务失败,原因:任务状态不是进行中"); - - // ========== OTA 升级任务记录相关 1-050-008-200 ========== - - ErrorCode OTA_TASK_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在"); - ErrorCode OTA_TASK_RECORD_CANCEL_FAIL_STATUS_ERROR = new ErrorCode(1_050_008_201, "取消 OTA 升级记录失败,原因:记录状态不是进行中"); - ErrorCode OTA_TASK_RECORD_UPDATE_PROGRESS_FAIL_NO_EXISTS = new ErrorCode(1_050_008_202, "更新 OTA 升级记录进度失败,原因:该设备没有进行中的升级记录"); - - // ========== IoT 数据流转规则 1-050-010-000 ========== - ErrorCode DATA_RULE_NOT_EXISTS = new ErrorCode(1_050_010_000, "数据流转规则不存在"); - ErrorCode DATA_RULE_NAME_EXISTS = new ErrorCode(1_050_010_001, "数据流转规则名称已存在"); - - // ========== IoT 数据流转目的 1-050-011-000 ========== - ErrorCode DATA_SINK_NOT_EXISTS = new ErrorCode(1_050_011_000, "数据桥梁不存在"); - ErrorCode DATA_SINK_DELETE_FAIL_USED_BY_RULE = new ErrorCode(1_050_011_001, "数据流转目的正在被数据流转规则使用,无法删除"); - ErrorCode DATA_SINK_NAME_EXISTS = new ErrorCode(1_050_011_002, "数据流转目的名称已存在"); - - // ========== IoT 场景联动 1-050-012-000 ========== - ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_012_000, "场景联动不存在"); - - // ========== IoT 告警配置 1-050-013-000 ========== - ErrorCode ALERT_CONFIG_NOT_EXISTS = new ErrorCode(1_050_013_000, "IoT 告警配置不存在"); - - // ========== IoT 告警记录 1-050-014-000 ========== - ErrorCode ALERT_RECORD_NOT_EXISTS = new ErrorCode(1_050_014_000, "IoT 告警记录不存在"); - // ======================================= Tdengine ============================================ ErrorCode TABLE_CREATION_FAILED = new ErrorCode(1_004_000_008, "TDengine 表创建失败"); ErrorCode COLOUMN_CREATION_FAILED = new ErrorCode(1_004_000_008, "TDengine 列创建失败"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/DeviceJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/DeviceJob.java new file mode 100644 index 0000000..c2caf6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/DeviceJob.java @@ -0,0 +1,411 @@ +package cn.iocoder.yudao.module.iot.job; + +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum; +import cn.iocoder.yudao.module.iot.controller.admin.devicemodelrules.vo.PointRulesRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicepointrules.DevicePointRulesDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactModelMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord.DeviceOperationRecordMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.devicepointrules.DevicePointRulesMapper; +import cn.iocoder.yudao.module.iot.service.device.TDengineService; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class DeviceJob implements JobHandler { + + @Resource + private TDengineService tDengineService; + + @Resource + private DeviceMapper deviceMapper; + + @Resource + private DeviceContactModelMapper deviceContactModelMapper; + + @Resource + private DeviceOperationRecordMapper deviceOperationRecordMapper; + + @Resource + private DevicePointRulesMapper devicePointRulesMapper; + + + @Override + public String execute(String param) throws Exception { + // 设置租户上下文 + TenantContextHolder.setTenantId(1L); + + // 解析超时时间(默认60秒) + long timeoutSeconds = 60L; + if (StringUtils.isNotBlank(param)) { + try { + timeoutSeconds = Long.parseLong(param); + } catch (NumberFormatException e) { + log.warn("定时任务参数非法,使用默认60秒 param={}", param); + } + } + + log.info("定时任务开始, timeoutSeconds={} 时间={}", timeoutSeconds, new Date()); + + // 查询采集设备列表 + List deviceDOS = + deviceMapper.selectList(Wrappers.lambdaQuery().orderByDesc(DeviceDO::getId)); + + if (CollectionUtils.isEmpty(deviceDOS)) { + return param; + } + + List deviceIds = deviceDOS.stream() + .map(DeviceDO::getId) + .collect(Collectors.toList()); + + // 获取设备的每条最新数据 + Map> deviceRowMap = + tDengineService.queryDevicesLatestRow(deviceIds, null, null); + + Instant now = Instant.now(); + + // 遍历设备 + for (Long deviceId : deviceIds) { + Map row = deviceRowMap.get(deviceId); + boolean isTimeout = false; + + if (row == null || row.get("ts") == null) { + isTimeout = true; + } else { + Instant ts = parseTs(row.get("ts"), deviceId); + if (ts == null || Duration.between(ts, now).getSeconds() > timeoutSeconds) { + isTimeout = true; + } + } + + if (isTimeout) { + handleDeviceTimeout(deviceId); + } else { + handleDeviceOnline(deviceId, row); + } + } + + return param; + } + + /** + * 安全转换 ts 为 Instant + */ + private Instant parseTs(Object tsObj, Long deviceId) { + if (tsObj == null) return null; + + log.debug("设备 {} tsObj 类型: {}, 值: {}", deviceId, tsObj.getClass().getName(), tsObj); + + if (tsObj instanceof Instant) { + return (Instant) tsObj; + } else if (tsObj instanceof Timestamp) { + return ((Timestamp) tsObj).toInstant(); + } else if (tsObj instanceof Date) { + return ((Date) tsObj).toInstant(); + } else if (tsObj instanceof LocalDateTime) { + return ((LocalDateTime) tsObj).atZone(ZoneId.systemDefault()).toInstant(); + } else if (tsObj instanceof String) { + String tsStr = (String) tsObj; + try { + return Instant.parse(tsStr); // ISO 8601 + } catch (Exception e1) { + try { + return Timestamp.valueOf(tsStr).toInstant(); // yyyy-MM-dd HH:mm:ss + } catch (Exception e2) { + log.warn("设备 {} ts 字符串解析失败: {}", deviceId, tsStr); + } + } + } else { + log.warn("设备 {} ts 类型未知: {}", deviceId, tsObj); + } + return null; + } + + /** + * 设备在线处理 + */ + private void handleDeviceOnline(Long deviceId, Map row) { + if (row == null) return; + + // 1. 查询设备规则 + DevicePointRulesDO pointRulesDO = devicePointRulesMapper.selectOne( + Wrappers.lambdaQuery() + .eq(DevicePointRulesDO::getDeviceId, deviceId) + .eq(DevicePointRulesDO::getIdentifier, "RUNNING") + .orderByDesc(DevicePointRulesDO::getId) + .last("LIMIT 1") + ); + + if(pointRulesDO == null || StringUtils.isBlank(pointRulesDO.getFieldRule())){ + //处理待机中 + DeviceOperationRecordDO record = new DeviceOperationRecordDO(); + record.setDeviceId(deviceId); + record.setRule(DeviceStatusEnum.STANDBY.getCode()); + record.setCreator("1"); + record.setUpdater("1"); +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); + + return; + } + + + // 解析规则列表 + List pointRulesVOList = JSON.parseArray( + pointRulesDO.getFieldRule(), PointRulesRespVO.class + ); + if (CollectionUtils.isEmpty(pointRulesVOList)) return; + + // 2. 查询设备 contact model + List deviceContactModelDOS = deviceContactModelMapper.selectList( + Wrappers.lambdaQuery().eq(DeviceContactModelDO::getDeviceId, deviceId) + ); + if (CollectionUtils.isEmpty(deviceContactModelDOS)) return; + + // 3. 遍历规则,匹配成功则保存记录 + for (PointRulesRespVO pointRule : pointRulesVOList) { + if (StringUtils.isBlank(pointRule.getCode())) continue; + + String ruleCode = pointRule.getCode().toLowerCase(); + String processedValue = row.get(ruleCode).toString(); + boolean matched = matchRule(processedValue, pointRule); + + if (!matched) { + log.debug("规则匹配失败: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule)); + continue; + } + + log.info("规则匹配成功: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule)); + + // 4. 遍历 contact model 查找对应 code + DeviceContactModelDO matchedContact = null; + for (DeviceContactModelDO contact : deviceContactModelDOS) { + if (ruleCode.equalsIgnoreCase(contact.getAttributeCode())) { + matchedContact = contact; + break; + } + } + + if (matchedContact == null) { + log.warn("设备 {} 找不到 attributeCode={} 对应的 modelId,跳过", deviceId, pointRule.getCode()); + continue; + } + + // 5. 保存运行记录 + DeviceOperationRecordDO record = new DeviceOperationRecordDO(); + record.setDeviceId(deviceId); + record.setModelId(matchedContact.getId()); + record.setRule(pointRule.getRule()); + record.setAddressValue(processedValue); + record.setRuleId(pointRulesDO.getId()); + record.setCreator("1"); + record.setUpdater("1"); + +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); + + break; + } + } + + + private void handleDeviceTimeout(Long deviceId) { + DeviceOperationRecordDO record = new DeviceOperationRecordDO(); + record.setDeviceId(deviceId); + record.setRule(DeviceStatusEnum.OFFLINE.getCode()); + record.setCreator("1"); + record.setUpdater("1"); +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); + } + + /** + * 判断值是否符合规则 + * 支持操作符: EQ(等于), NE(不等于), GT(大于), GE(大于等于), + * LT(小于), LE(小于等于), TRUE(为真), FALSE(为假) + */ + private boolean matchRule(String value, PointRulesRespVO rule) { + if (StringUtils.isBlank(value) || rule == null || + StringUtils.isBlank(rule.getOperator())) { + return false; + } + + try { + String operator = rule.getOperator().toUpperCase(); + String inputValue = value.trim().toLowerCase(); + String ruleValue = StringUtils.trimToEmpty(rule.getOperatorRule()); + + // 1. 处理布尔值判断 + if ("TRUE".equals(operator) || "FALSE".equals(operator)) { + return matchBooleanRule(inputValue, operator); + } + + // 2. 如果operatorRule为空,且不是布尔操作符,则返回false + if (StringUtils.isBlank(ruleValue)) { + log.warn("规则比较值为空,但操作符不是布尔类型: operator={}", operator); + return false; + } + + ruleValue = ruleValue.trim(); + + // 3. 尝试数值比较 + if (isNumeric(inputValue) && isNumeric(ruleValue)) { + Double num1 = Double.parseDouble(inputValue); + Double num2 = Double.parseDouble(ruleValue); + + return compareNumbers(num1, num2, operator); + } + // 4. 字符串比较 + else { + return compareStrings(inputValue, ruleValue, operator); + } + + } catch (Exception e) { + log.error("规则匹配异常: value={}, rule={}, error={}", + value, JSON.toJSONString(rule), e.getMessage()); + return false; + } + } + + /** + * 字符串比较 + */ + private boolean compareStrings(String value, String ruleValue, String operator) { + switch (operator) { + case "EQ": + return value.equals(ruleValue); + case "NE": + return !value.equals(ruleValue); + case "GT": + return value.compareTo(ruleValue) > 0; + case "GE": + return value.compareTo(ruleValue) >= 0; + case "LT": + return value.compareTo(ruleValue) < 0; + case "LE": + return value.compareTo(ruleValue) <= 0; + default: + log.warn("不支持的操作符: {}", operator); + return false; + } + } + + + /** + * 数值比较 + */ + private boolean compareNumbers(Double value, Double ruleValue, String operator) { + switch (operator) { + case "EQ": + return Math.abs(value - ruleValue) < 0.000001; // 处理浮点数精度 + case "NE": + return Math.abs(value - ruleValue) >= 0.000001; + case "GT": + return value > ruleValue; + case "GE": + return value >= ruleValue; + case "LT": + return value < ruleValue; + case "LE": + return value <= ruleValue; + default: + log.warn("不支持的操作符: {}", operator); + return false; + } + } + + + /** + * 判断字符串是否为数字 + */ + private boolean isNumeric(String str) { + if (StringUtils.isBlank(str)) { + return false; + } + try { + Double.parseDouble(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 处理布尔值判断 + */ + private boolean matchBooleanRule(String value, String operator) { + // 常见布尔值表示 + boolean booleanValue = parseBoolean(value); + + if ("TRUE".equals(operator)) { + return booleanValue; + } else if ("FALSE".equals(operator)) { + return !booleanValue; + } + + return false; + } + + + /** + * 解析字符串为布尔值 + * 支持: true, false, 1, 0, yes, no, on, off等 + */ + private boolean parseBoolean(String value) { + if (StringUtils.isBlank(value)) { + return false; + } + + String lowerValue = value.toLowerCase(); + + // 常见真值表示 + if ("true".equals(lowerValue) || + "1".equals(lowerValue) || + "yes".equals(lowerValue) || + "on".equals(lowerValue) || + "是".equals(lowerValue) || // 中文支持 + "成功".equals(lowerValue)) { + return true; + } + + // 常见假值表示 + if ("false".equals(lowerValue) || + "0".equals(lowerValue) || + "no".equals(lowerValue) || + "off".equals(lowerValue) || + "否".equals(lowerValue) || // 中文支持 + "失败".equals(lowerValue)) { + return false; + } + + // 尝试转换为布尔值 + try { + return Boolean.parseBoolean(lowerValue); + } catch (Exception e) { + log.warn("无法解析为布尔值: {}", value); + return false; + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java index 68450e2..e73f70e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO; @@ -13,6 +14,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.paho.client.mqttv3.MqttException; import javax.validation.Valid; +import java.time.LocalDateTime; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -134,7 +136,7 @@ public interface DeviceService { Boolean scheduledStop(Long id); - DeviceOperationStatusRespVO getDeviceOperationalStatus(); + DeviceOperationStatusRespVO getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime); List> getMultiDeviceAttributes(Long goviewId); @@ -145,4 +147,12 @@ public interface DeviceService { void updateDeviceEnabled(@Valid DeviceUpdateEnabledReqVO updateEnabledReqVO) throws MqttException; DeviceDO getDeviceByMqttTopic(String topic); + + List deviceList(@Valid DevicePageReqVO pageReqVO); + + DeviceRunStatusStatsRespVO getDeviceRunStatusStats(Long customerId, Long orgNodeId); + + DeviceRunStatusStatsRespVO getRunStatusStatsByCustomer(Long customerId); + + List getStatusCountByCustomer(); } \ 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 index adbedfc..b9d1bb0 100644 --- 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 @@ -1,16 +1,21 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.DeviceConnectionStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO; +import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper; +import cn.iocoder.yudao.module.iot.controller.admin.device.dto.DeviceLatestRuleDTO; import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum; import cn.iocoder.yudao.module.iot.controller.admin.device.scheduled.scheduler.TaskSchedulerManager; import cn.iocoder.yudao.module.iot.controller.admin.device.scheduled.utils.CronExpressionUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.utils.DataTypeParseUtil; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceattributetype.DeviceAttributeTypeDO; @@ -20,7 +25,9 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelattribute.DeviceMod import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelrules.DeviceModelRulesDO; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO; import cn.iocoder.yudao.module.iot.dal.dataobject.devicepointrules.DevicePointRulesDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; import cn.iocoder.yudao.module.iot.dal.dataobject.gateway.GatewayDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceAttributeMapper; import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; import cn.iocoder.yudao.module.iot.dal.mysql.deviceattributetype.DeviceAttributeTypeMapper; @@ -31,6 +38,8 @@ import cn.iocoder.yudao.module.iot.dal.mysql.devicemodelrules.DeviceModelRulesMa import cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord.DeviceOperationRecordMapper; import cn.iocoder.yudao.module.iot.dal.mysql.devicepointrules.DevicePointRulesMapper; import cn.iocoder.yudao.module.iot.dal.mysql.gateway.GatewayMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.orgnode.OrgNodeMapper; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMapper; import cn.iocoder.yudao.module.iot.framework.mqtt.consumer.IMqttservice; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -43,6 +52,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.paho.client.mqttv3.MqttException; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -85,6 +95,9 @@ public class DeviceServiceImpl implements DeviceService { @Resource private DeviceContactModelMapper deviceContactModelMapper; + @Resource + private IotDeviceMapper iotDeviceMapper; + // @Resource // private MqttDataRecordMapper mqttDataRecordMapper; @Resource @@ -118,6 +131,12 @@ public class DeviceServiceImpl implements DeviceService { @Resource private GatewayMapper gatewayMapper; + @Resource + @Lazy + private OrgNodeMapper orgNodeMapper; + + @Resource + private ManagementMapper cusManagementMapper; @Override @@ -139,6 +158,9 @@ public class DeviceServiceImpl implements DeviceService { throw exception(DEVICE_CODE_ALREADY_EXISTS); } + if (StrUtil.isNotBlank(createReqVO.getDeviceCode())) { + tdengineService.validateTableName(createReqVO.getDeviceCode()); + } DeviceModelDO deviceModelDO = deviceModelMapper.selectById(createReqVO.getDeviceModelId()); @@ -192,6 +214,13 @@ public class DeviceServiceImpl implements DeviceService { List devicePointRulesDOList = new ArrayList<>(); DeviceModelDO deviceModelDO = deviceModelMapper.selectById(deviceModelId); if(deviceModelDO == null){ + DevicePointRulesDO devicePointRulesDO = new DevicePointRulesDO(); + devicePointRulesDO.setIdentifier("RUNNING"); + devicePointRulesDO.setFieldName("运行"); + devicePointRulesDO.setDefaultValue("运行"); + devicePointRulesDO.setDeviceId(deviceId); + devicePointRulesDOList.add(devicePointRulesDO); + devicePointRulesMapper.insert(devicePointRulesDO); return; } List deviceModelRulesDOList = deviceModelRulesMapper.selectList(Wrappers.lambdaQuery() @@ -235,7 +264,7 @@ public class DeviceServiceImpl implements DeviceService { // 校验存在 validateDeviceExists(id); //是否有引用 - validateReference(id); +// validateReference(id); // // 删除 // deviceMapper.deleteById(id); // 删除子表 @@ -269,19 +298,49 @@ public class DeviceServiceImpl implements DeviceService { @Override public DeviceRespVO getDevice(Long id) { DeviceDO deviceDO = deviceMapper.selectById(id); + if (deviceDO == null) { + return null; // 或者 throw exception(DEVICE_NOT_EXISTS); + } + DeviceRespVO deviceRespVO = BeanUtils.toBean(deviceDO, DeviceRespVO.class); - List devicePointRulesDOList = devicePointRulesMapper.selectList(Wrappers.lambdaQuery() - .eq(DevicePointRulesDO::getDeviceId, id)); -// -// //设置点位规则 -// if (!devicePointRulesDOList.isEmpty()){ -// -// deviceRespVO.setPointRulesVOList(devicePointRulesDOList); -// } + Map latestTsMap = tdengineService.newSelectLatestTsBatch(Collections.singletonList(id)); - return deviceRespVO; + List devicePointRulesDOList = devicePointRulesMapper.selectList( + Wrappers.lambdaQuery() + .eq(DevicePointRulesDO::getDeviceId, id) + ); + + // 如需返回点位规则再打开 + // if (CollUtil.isNotEmpty(devicePointRulesDOList)) { + // deviceRespVO.setPointRulesVOList(devicePointRulesDOList); + // } + //设置采集时间 + if(latestTsMap.get(id) != null){ + deviceRespVO.setCollectionTime(latestTsMap.get(id)); + + } + + // 补客户名称 + if (deviceDO.getCustomerId() != null) { + ManagementDO customer = cusManagementMapper.selectById(deviceDO.getCustomerId()); + if (customer != null) { + deviceRespVO.setCustomerName(customer.getCustomerName()); + } + } + + + // 补组织名称(可能为空,表示直挂客户) + if (deviceDO.getOrgNodeId() != null && deviceDO.getOrgNodeId() > 0) { + OrgNodeDO orgNode = orgNodeMapper.selectById(deviceDO.getOrgNodeId()); + if (orgNode != null) { + deviceRespVO.setOrgNodeName(orgNode.getName()); + } + } + + return deviceRespVO; } + @Override public DeviceDO getDeviceByName(String name) { return deviceMapper.selectByName(name); @@ -1133,7 +1192,7 @@ public class DeviceServiceImpl implements DeviceService { } @Override - public DeviceOperationStatusRespVO getDeviceOperationalStatus() { + public DeviceOperationStatusRespVO getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime) { DeviceOperationStatusRespVO result = new DeviceOperationStatusRespVO(); @@ -1185,6 +1244,11 @@ public class DeviceServiceImpl implements DeviceService { // 计算故障率 calculateFaultRate(result); + +// PageResult deviceWarinningRecordDOPageResult = tdengineService.selectRunngingRecordPage(reqVO).getList(); + Long warnningRecordcount = iotDeviceMapper.countWarningRecordByTime(startTime, endTime); + + result.setWarningRecordCount(warnningRecordcount); return result; } @@ -1478,7 +1542,6 @@ public class DeviceServiceImpl implements DeviceService { if (StringUtils.isBlank(deviceDO.getTopic())) { throw exception(DEVICE_MQTT_TOPIC_EXIST); } - //TODO 待优化 if (!"MQTT".equals(deviceDO.getProtocol())) { throw exception(DEVICE_MQTT_TOPIC_EXIST); @@ -1501,6 +1564,67 @@ public class DeviceServiceImpl implements DeviceService { return deviceMapper.selectOne(Wrappers.lambdaQuery().eq(DeviceDO::getTopic,topic)); } + @Override + public List deviceList(DevicePageReqVO pageReqVO) { + + // 查询设备集合 + List deviceDOS = deviceMapper.selectList(Wrappers.lambdaQuery() + .eq(DeviceDO::getCustomerId, pageReqVO.getCustomerId())); + + List deviceIds = deviceDOS.stream() + .map(DeviceDO::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 查询规则 + List ruleCodes = Arrays.stream(DeviceStatusEnum.values()) + .map(DeviceStatusEnum::getCode) + .collect(Collectors.toList()); + + List operationRecords = + tdengineService.selectLatestByDeviceAndRuleMinimal(deviceIds, ruleCodes); + + // 按 deviceId 分组,取最新一条 + Map latestRecordMap = operationRecords.stream() + .collect(Collectors.toMap( + DeviceOperationRecordDO::getDeviceId, + r -> r, + (r1, r2) -> r1.getCreateTime().isAfter(r2.getCreateTime()) ? r1 : r2 + )); + + List deviceRespVOList = BeanUtils.toBean(deviceDOS, DeviceRespVO.class); + + for (DeviceRespVO deviceRespVO : deviceRespVOList) { + Long deviceId = deviceRespVO.getId(); + DeviceOperationRecordDO record = latestRecordMap.get(deviceId); + if (record != null) { + DeviceStatusEnum statusEnum = DeviceStatusEnum.getByCode(record.getRule()); + deviceRespVO.setOperatingStatus(statusEnum != null + ? statusEnum.getName() + : DeviceStatusEnum.OFFLINE.getName()); + } else { + deviceRespVO.setOperatingStatus(DeviceStatusEnum.OFFLINE.getName()); + } + } + + // 按 status 过滤(pageReqVO.status 传状态 code) + if (StrUtil.isNotBlank(pageReqVO.getStatus())) { + DeviceStatusEnum queryStatus = DeviceStatusEnum.getByCode(pageReqVO.getStatus()); + if (queryStatus != null) { + String targetStatusName = queryStatus.getName(); + deviceRespVOList = deviceRespVOList.stream() + .filter(item -> Objects.equals(item.getOperatingStatus(), targetStatusName)) + .collect(Collectors.toList()); + } else { + // 传入非法状态时返回空 + return Collections.emptyList(); + } + } + + return deviceRespVOList; + } + + private void executeEnableUpdate(DeviceDO deviceDO, Boolean enabled) { try { @@ -1596,4 +1720,262 @@ public class DeviceServiceImpl implements DeviceService { return deviceMapper.deviceLedgerList(); } + + + @Override + public DeviceRunStatusStatsRespVO getDeviceRunStatusStats(Long customerId, Long orgNodeId) { + // 1) 先确定设备范围(MySQL) + List deviceIds; + + if (orgNodeId != null) { + // 当前组织及子组织 + List all = orgNodeMapper.selectListByCustomerByNodeId(orgNodeId); + if (CollUtil.isEmpty(all)) { + return emptyStats(); + } + + Map> childrenMap = new HashMap<>(); + for (OrgNodeDO n : all) { + childrenMap.computeIfAbsent(n.getParentId(), k -> new ArrayList<>()).add(n.getId()); + } + + Set subtreeIds = new HashSet<>(); + Deque queue = new ArrayDeque<>(); + queue.add(orgNodeId); + while (!queue.isEmpty()) { + Long cur = queue.poll(); + if (!subtreeIds.add(cur)) { + continue; + } + List children = childrenMap.get(cur); + if (CollUtil.isNotEmpty(children)) { + queue.addAll(children); + } + } + + List devices = deviceMapper.selectByOrgNodeIds(new ArrayList<>(subtreeIds)); + deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + + } else if (customerId != null) { + List devices = deviceMapper.selectByCustomerId(customerId); + deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + + } else { + deviceIds = deviceMapper.getAllDeviceIds(); + } + + if (CollUtil.isEmpty(deviceIds)) { + return emptyStats(); + } + + // 2) tdengine 查每个设备最新一条 rule + List latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds); + + // 3) 聚合统计 + Map latestRuleMap = latestRules.stream() + .collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a)); + + DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO(); + resp.setTotalDeviceCount((long) deviceIds.size()); + resp.setOfflineCount(0L); + resp.setRunningCount(0L); + resp.setStandbyCount(0L); + resp.setFaultStandbyCount(0L); + resp.setAlarmRunningCount(0L); + + for (Long deviceId : deviceIds) { + String rule = latestRuleMap.get(deviceId); + if (rule == null) { + // 没有记录按离线 + resp.setOfflineCount(resp.getOfflineCount() + 1); + continue; + } + + DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule); + if (status == null) { + resp.setOfflineCount(resp.getOfflineCount() + 1); + continue; + } + + switch (status) { + case OFFLINE: + resp.setOfflineCount(resp.getOfflineCount() + 1); + break; + case RUNNING: + resp.setRunningCount(resp.getRunningCount() + 1); + break; + case STANDBY: + resp.setStandbyCount(resp.getStandbyCount() + 1); + break; + case FAULT_STANDBY: + resp.setFaultStandbyCount(resp.getFaultStandbyCount() + 1); + break; + case ALARM_RUNNING: + resp.setAlarmRunningCount(resp.getAlarmRunningCount() + 1); + break; + default: + resp.setOfflineCount(resp.getOfflineCount() + 1); + } + } + + return resp; + } + + private DeviceRunStatusStatsRespVO emptyStats() { + DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO(); + resp.setTotalDeviceCount(0L); + resp.setOfflineCount(0L); + resp.setRunningCount(0L); + resp.setStandbyCount(0L); + resp.setFaultStandbyCount(0L); + resp.setAlarmRunningCount(0L); + return resp; + } + + @Override + public DeviceRunStatusStatsRespVO getRunStatusStatsByCustomer(Long customerId) { + List devices = deviceMapper.selectByCustomerId(customerId); + if (CollUtil.isEmpty(devices)) { + return emptyStats(); + } + + List deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + + List latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds); + Map latestRuleMap = latestRules.stream() + .collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a)); + + DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO(); + resp.setTotalDeviceCount((long) deviceIds.size()); + resp.setOfflineCount(0L); + resp.setRunningCount(0L); + resp.setStandbyCount(0L); + resp.setFaultStandbyCount(0L); + resp.setAlarmRunningCount(0L); + + for (Long deviceId : deviceIds) { + String rule = latestRuleMap.get(deviceId); + DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule); + if (status == null) { + resp.setOfflineCount(resp.getOfflineCount() + 1); + continue; + } + switch (status) { + case OFFLINE: + resp.setOfflineCount(resp.getOfflineCount() + 1); break; + case RUNNING: + resp.setRunningCount(resp.getRunningCount() + 1); break; + case STANDBY: + resp.setStandbyCount(resp.getStandbyCount() + 1); break; + case FAULT_STANDBY: + resp.setFaultStandbyCount(resp.getFaultStandbyCount() + 1); break; + case ALARM_RUNNING: + resp.setAlarmRunningCount(resp.getAlarmRunningCount() + 1); break; + default: + resp.setOfflineCount(resp.getOfflineCount() + 1); + } + } + return resp; + } + + + @Override + public List getStatusCountByCustomer() { + // 1) 查所有客户 + List customers = cusManagementMapper.selectList(); + if (CollUtil.isEmpty(customers)) { + return Collections.emptyList(); + } + + Map customerNameMap = customers.stream() + .collect(Collectors.toMap(ManagementDO::getId, ManagementDO::getCustomerName, (a, b) -> a)); + + // 2) 查所有设备(含 customerId) + List devices = deviceMapper.selectList(); + if (CollUtil.isEmpty(devices)) { + // 没设备时,每个客户返回 0 + return customers.stream() + .map(this::buildEmptyCustomerStats) + .collect(Collectors.toList()); + } + + List deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + + // 3) 查 tdengine 每设备最新 rule + List latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds); + Map latestRuleMap = latestRules.stream() + .collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a)); + + // 4) 初始化每个客户统计对象 + Map statsMap = new LinkedHashMap<>(); + for (ManagementDO c : customers) { + statsMap.put(c.getId(), buildEmptyCustomerStats(c)); + } + + // 5) 按设备归属客户累加 + for (DeviceDO d : devices) { + Long customerId = d.getCustomerId(); + if (customerId == null) { + continue; + } + + // 防止设备 customerId 在 customers 中不存在(脏数据兜底) + CustomerDeviceStatusStatsRespVO stats = statsMap.computeIfAbsent(customerId, k -> { + ManagementDO fallback = new ManagementDO(); + fallback.setId(k); + fallback.setCustomerName(customerNameMap.get(k)); + return buildEmptyCustomerStats(fallback); + }); + + stats.setTotalDeviceCount(stats.getTotalDeviceCount() + 1); + + String rule = latestRuleMap.get(d.getId()); + DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule); + if (status == null) { + stats.setOfflineCount(stats.getOfflineCount() + 1); + continue; + } + + switch (status) { + case OFFLINE: + stats.setOfflineCount(stats.getOfflineCount() + 1); + break; + case RUNNING: + stats.setRunningCount(stats.getRunningCount() + 1); + break; + case STANDBY: + stats.setStandbyCount(stats.getStandbyCount() + 1); + break; + case FAULT_STANDBY: + stats.setFaultStandbyCount(stats.getFaultStandbyCount() + 1); + break; + case ALARM_RUNNING: + stats.setAlarmRunningCount(stats.getAlarmRunningCount() + 1); + break; + default: + stats.setOfflineCount(stats.getOfflineCount() + 1); + } + } + + return new ArrayList<>(statsMap.values()); + } + + private CustomerDeviceStatusStatsRespVO buildEmptyCustomerStats(ManagementDO customer) { + CustomerDeviceStatusStatsRespVO vo = new CustomerDeviceStatusStatsRespVO(); + vo.setCustomerId(customer.getId()); + vo.setCustomerName(customer.getCustomerName()); + + vo.setLongitude(customer.getLongitude()); + vo.setLatitude(customer.getLatitude()); + + vo.setTotalDeviceCount(0L); + vo.setOfflineCount(0L); + vo.setRunningCount(0L); + vo.setStandbyCount(0L); + vo.setFaultStandbyCount(0L); + vo.setAlarmRunningCount(0L); + return vo; + } + + } \ 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/TDengineService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java index bfcf3c4..b5d0ae1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java @@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.common.protocol.types.Field; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -1259,6 +1260,52 @@ public class TDengineService { } } + /** + * 验证表名是否符合TDengine规则 + */ + public void validateTableName(String tableName) { + if (StrUtil.isBlank(tableName)) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, "表名不能为空"); + } + + // TDengine 表名规则验证 + // 1. 不能是保留关键字 + Set reservedKeywords = new HashSet<>(Arrays.asList( + "value", "timestamp", "current", "database", "table", "user", "password", + "select", "insert", "update", "delete", "create", "drop", "alter", + "show", "describe", "use", "ts", "now", "current_timestamp" + )); + + if (reservedKeywords.contains(tableName.toLowerCase())) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, + "表名不能使用保留关键字: " + tableName); + } + + // 2. 必须以字母开头 + if (!Character.isLetter(tableName.charAt(0))) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, + "表名必须以字母开头: " + tableName); + } + + // 3. 只能包含字母、数字、下划线 + if (!tableName.matches("^[a-zA-Z][a-zA-Z0-9_]*$")) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, + "表名只能包含字母、数字和下划线: " + tableName); + } + + // 4. 长度限制(TDengine表名最大长度通常为192) + if (tableName.length() > 192) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, + "表名长度不能超过192个字符: " + tableName); + } + + // 5. 不能以下划线开头(与列名策略保持一致) + if (tableName.startsWith("_")) { + throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, + "表名不能以下划线开头: " + tableName); + } + } + /** * 根据deviceId批量查询最新时间 @@ -2236,6 +2283,8 @@ public class TDengineService { + + /** * TDengine 分页查询告警记录 */ @@ -2324,21 +2373,20 @@ public class TDengineService { } // 创建时间范围 - if (reqVO.getCreateTime() != null && reqVO.getCreateTime().length == 2) { - LocalDateTime startTime = reqVO.getCreateTime()[0]; - LocalDateTime endTime = reqVO.getCreateTime()[1]; + String startTime = reqVO.getStartTime(); + String endTime = reqVO.getEndTime(); - if (startTime != null) { - where.append(" AND create_time >= ? "); - params.add(Timestamp.valueOf(startTime)); - } + if (startTime != null) { + where.append(" AND create_time >= ? "); + params.add(Timestamp.valueOf(startTime)); + } - if (endTime != null) { - where.append(" AND create_time <= ? "); - params.add(Timestamp.valueOf(endTime)); - } + if (endTime != null) { + where.append(" AND create_time <= ? "); + params.add(Timestamp.valueOf(endTime)); } + // 规则ID if (reqVO.getRuleId() != null) { where.append(" AND rule_id = ? "); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodel/DeviceModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodel/DeviceModelServiceImpl.java index 8226255..8ddcb00 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodel/DeviceModelServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodel/DeviceModelServiceImpl.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Random; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODEL_CODE_EXISTS; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODEL_NOT_EXISTS; /** diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java index cb34b57..a502551 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.service.devicemodelattribute; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; @@ -63,6 +64,9 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; @Resource private ErpProductUnitService productUnitService; + @Resource + @Lazy + private TDengineService tDengineService; @Override public Long createDeviceModelAttribute(DeviceModelAttributeSaveReqVO createReqVO) { @@ -77,9 +81,15 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; throw exception(DEVICE_MODEL_ATTRIBUTE_POTIN_CODE_EXISTS); } - // 插入 + // 插入 DeviceModelAttributeDO deviceModelAttribute = BeanUtils.toBean(createReqVO, DeviceModelAttributeDO.class); // deviceModelAttribute.setTypeName(deviceAttributeTypeMapper.selectById(createReqVO.getAttributeCode()).getName()); + //检查attributeCode是否符合TDengine列名规则 + if (StrUtil.isNotBlank(createReqVO.getAttributeCode())) { + tDengineService.validateColumnName(createReqVO.getAttributeCode()); + } + + deviceModelAttributeMapper.insert(deviceModelAttribute); // 返回 return deviceModelAttribute.getId(); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordService.java index e197f2f..4556eca 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordService.java @@ -1,8 +1,7 @@ package cn.iocoder.yudao.module.iot.service.devicewarinningrecord; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; import javax.validation.Valid; @@ -52,11 +51,16 @@ public interface DeviceWarinningRecordService { * @param pageReqVO 分页查询 * @return 告警记录分页 */ - PageResult getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO); + PageResult getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO); + + List getList(DeviceWarningListReqVO reqVO); - List getList(Long id); List> getLastSevenHoursCount(); + DeviceWarningCountRespVO getWarningCount(DeviceWarningCountReqVO reqVO); + + DeviceWarningTrendRespVO getWarningTrend(DeviceWarningTrendReqVO reqVO); + } \ 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/devicewarinningrecord/DeviceWarinningRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordServiceImpl.java index 4ad3c5b..574316a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicewarinningrecord/DeviceWarinningRecordServiceImpl.java @@ -1,26 +1,32 @@ package cn.iocoder.yudao.module.iot.service.devicewarinningrecord; +import cn.hutool.core.collection.CollUtil; 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.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO; +import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO; +import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; import cn.iocoder.yudao.module.iot.dal.mysql.devicewarinningrecord.DeviceWarinningRecordMapper; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMapper; import cn.iocoder.yudao.module.iot.service.device.TDengineService; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_WARINNING_RECORD_NOT_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; /** * 告警记录 Service 实现类 @@ -33,6 +39,14 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe @Resource private DeviceWarinningRecordMapper deviceWarinningRecordMapper; + @Resource + private IotDeviceMapper iotDeviceMapper; + @Resource + @Lazy + private DeviceMapper deviceMapper; + @Resource + private ManagementMapper cusManagementMapper; + @Resource private TDengineService tDengineService; @@ -76,24 +90,98 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe } @Override - public PageResult getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO) { -// return deviceWarinningRecordMapper.selectPage(pageReqVO); - return tDengineService.selectRunngingRecordPage(pageReqVO); + public PageResult getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO) { + PageResult page = tDengineService.selectRunngingRecordPage(pageReqVO); + if (CollUtil.isEmpty(page.getList())) { + return new PageResult<>(Collections.emptyList(), page.getTotal()); + } + + Set deviceIds = page.getList().stream() + .map(DeviceWarinningRecordDO::getDeviceId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map deviceMap = CollUtil.isEmpty(deviceIds) ? Collections.emptyMap() + : deviceMapper.selectBatchIds(deviceIds).stream() + .collect(Collectors.toMap(DeviceDO::getId, Function.identity(), (a, b) -> a)); + + Set customerIds = deviceMap.values().stream() + .map(DeviceDO::getCustomerId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map customerMap = CollUtil.isEmpty(customerIds) ? Collections.emptyMap() + : cusManagementMapper.selectBatchIds(customerIds).stream() + .collect(Collectors.toMap(ManagementDO::getId, Function.identity(), (a, b) -> a)); + + List list = page.getList().stream().map(record -> { + DeviceWarinningRecordPageRespVO vo = BeanUtils.toBean(record, DeviceWarinningRecordPageRespVO.class); + + DeviceDO device = deviceMap.get(record.getDeviceId()); + if (device != null) { + vo.setDeviceName(device.getDeviceName()); + vo.setDeviceCode(device.getDeviceCode()); // 按你项目真实字段改:deviceCode/code/sn + ManagementDO customer = customerMap.get(device.getCustomerId()); + if (customer != null) { + vo.setCustomerCode(customer.getCustomerCode()); // 按真实字段名调整 + vo.setCustomerName(customer.getCustomerName()); + } + } + return vo; + }).collect(Collectors.toList()); + + return new PageResult<>(list, page.getTotal()); } + @Override - public List getList(Long id) { -// if (id == null) { -// return Collections.emptyList(); -// } -// return deviceWarinningRecordMapper.selectList( -// Wrappers.lambdaQuery() -// .eq(id != null, DeviceWarinningRecordDO::getDeviceId, id) -// .orderByDesc(DeviceWarinningRecordDO::getCreateTime) -// .last("LIMIT 100")// 限制 100 条 -// ); - - return tDengineService.selectDeviceWarningList(id); + public List getList(DeviceWarningListReqVO reqVO) { + if (reqVO.getStartTime() == null || reqVO.getEndTime() == null) { + throw exception(DEVICE_WARNING_TIME_REQUIRED); + } + + List records = iotDeviceMapper.selectWarningRecordList( + reqVO.getDeviceId(), + reqVO.getStartTime(), + reqVO.getEndTime(), + reqVO.getAlarmLevel() + ); + if (CollUtil.isEmpty(records)) { + return Collections.emptyList(); + } + + Set deviceIds = records.stream() + .map(DeviceWarinningRecordDO::getDeviceId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map deviceMap = CollUtil.isEmpty(deviceIds) ? Collections.emptyMap() + : deviceMapper.selectBatchIds(deviceIds).stream() + .collect(Collectors.toMap(DeviceDO::getId, Function.identity(), (a, b) -> a)); + + Set customerIds = deviceMap.values().stream() + .map(DeviceDO::getCustomerId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map customerMap = CollUtil.isEmpty(customerIds) ? Collections.emptyMap() + : cusManagementMapper.selectBatchIds(customerIds).stream() + .collect(Collectors.toMap(ManagementDO::getId, Function.identity(), (a, b) -> a)); + + return records.stream().map(record -> { + DeviceWarningListRespVO vo = BeanUtils.toBean(record, DeviceWarningListRespVO.class); + + DeviceDO device = deviceMap.get(record.getDeviceId()); + if (device != null) { + vo.setDeviceName(device.getDeviceName()); // 以主库设备名为准 + ManagementDO customer = customerMap.get(device.getCustomerId()); + if (customer != null) { + vo.setCustomerName(customer.getCustomerName()); + } + } + + return vo; + }).collect(Collectors.toList()); } @Override @@ -142,4 +230,91 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe return result; } + + @Override + public DeviceWarningCountRespVO getWarningCount(DeviceWarningCountReqVO reqVO) { + if (reqVO.getStartTime() == null || reqVO.getEndTime() == null) { + throw exception(DEVICE_WARNING_TIME_REQUIRED); + } + DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String start = reqVO.getStartTime().format(F); + String end = reqVO.getEndTime().format(F); + DeviceWarningCountRespVO resp = iotDeviceMapper.selectWarningCount(start, end); + + if (resp == null) { + resp = new DeviceWarningCountRespVO(); + resp.setTotalCount(0L); + resp.setNormalCount(0L); + resp.setTipCount(0L); + resp.setSeriousCount(0L); + return resp; + } + resp.setTotalCount(resp.getTotalCount() == null ? 0L : resp.getTotalCount()); + resp.setNormalCount(resp.getNormalCount() == null ? 0L : resp.getNormalCount()); + resp.setTipCount(resp.getTipCount() == null ? 0L : resp.getTipCount()); + resp.setSeriousCount(resp.getSeriousCount() == null ? 0L : resp.getSeriousCount()); + return resp; + } + + + @Override + public DeviceWarningTrendRespVO getWarningTrend(DeviceWarningTrendReqVO reqVO) { + LocalDateTime startTime = reqVO.getStartTime(); + LocalDateTime endTime = reqVO.getEndTime(); + if (startTime == null || endTime == null) { + throw exception(DEVICE_WARNING_TIME_REQUIRED); + } + if (startTime.isAfter(endTime)) { + throw exception(DEVICE_WARNING_TIME_RANGE_INVALID); + } + + DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String start = reqVO.getStartTime().format(F); + String end = reqVO.getEndTime().format(F); + + boolean byHour = startTime.toLocalDate().equals(endTime.toLocalDate()); + + List timePoints = new ArrayList<>(); + Map bucket = new LinkedHashMap<>(); + + if (byHour) { + LocalDate date = startTime.toLocalDate(); + for (int i = 0; i < 24; i++) { + String key = date + " " + String.format("%02d:00", i); // yyyy-MM-dd HH:00 + timePoints.add(key); + bucket.put(key, 0L); + } + + List rows = iotDeviceMapper.selectWarningTrendByHour(start, end); + for (WarningTrendPointRespDTO row : rows) { + if (bucket.containsKey(row.getTimeKey())) { + bucket.put(row.getTimeKey(), row.getCount() == null ? 0L : row.getCount()); + } + } + } else { + LocalDate d = startTime.toLocalDate(); + LocalDate endDate = endTime.toLocalDate(); + while (!d.isAfter(endDate)) { + String key = d.toString(); // yyyy-MM-dd + timePoints.add(key); + bucket.put(key, 0L); + d = d.plusDays(1); + } + + List rows = iotDeviceMapper.selectWarningTrendByDay(start, end); + for (WarningTrendPointRespDTO row : rows) { + if (bucket.containsKey(row.getTimeKey())) { + bucket.put(row.getTimeKey(), row.getCount() == null ? 0L : row.getCount()); + } + } + } + + DeviceWarningTrendRespVO resp = new DeviceWarningTrendRespVO(); + resp.setTimePoints(timePoints); + resp.setCounts(new ArrayList<>(bucket.values())); + return resp; + } + + + } \ 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/orgnode/OrgNodeService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeService.java index 1c86594..660ce05 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeService.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.iot.service.orgnode; import java.util.*; import javax.validation.*; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO; import cn.iocoder.yudao.module.iot.controller.admin.orgnode.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -61,4 +63,7 @@ public interface OrgNodeService { List getOrgTree(Long customerId); + + List getDeviceListByNode(Long nodeId, Integer nodeType); + } \ 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/orgnode/OrgNodeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeServiceImpl.java index 7787920..5362550 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/orgnode/OrgNodeServiceImpl.java @@ -2,12 +2,23 @@ package cn.iocoder.yudao.module.iot.service.orgnode; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO; +import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper; +import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; +import cn.iocoder.yudao.module.iot.service.device.TDengineService; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.stream.Collectors; + import cn.iocoder.yudao.module.iot.controller.admin.orgnode.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -32,14 +43,38 @@ public class OrgNodeServiceImpl implements OrgNodeService { @Resource private OrgNodeMapper orgNodeMapper; + @Resource + private TDengineService tDengineService; + @Resource + private DeviceMapper deviceMapper; + @Resource + private ManagementMapper cusManagementMapper; @Override public Long createOrgNode(OrgNodeSaveReqVO createReqVO) { - // 插入 OrgNodeDO orgNode = BeanUtils.toBean(createReqVO, OrgNodeDO.class); - orgNodeMapper.insert(orgNode); - // 返回 + // 1) parentId 兜底:null 视为根下一级(挂客户) + if (orgNode.getParentId() == null) { + orgNode.setParentId(0L); + } + + // 2) 根据 parentId 规范 customerId + if (orgNode.getParentId() > 0) { + OrgNodeDO parent = orgNodeMapper.selectById(orgNode.getParentId()); + if (parent == null) { + throw exception(ORG_NODE_NOT_EXISTS); + } + // 子节点必须跟父节点同客户 + orgNode.setCustomerId(parent.getCustomerId()); + } else { + // parentId == 0:必须明确属于哪个客户 + if (orgNode.getCustomerId() == null) { + throw exception(ORG_NODE_CUSTOMER_ID_REQUIRED); // 你们项目没有这个错误码就换成已有的 + } + } + + orgNodeMapper.insert(orgNode); return orgNode.getId(); } @@ -54,12 +89,24 @@ public class OrgNodeServiceImpl implements OrgNodeService { @Override public void deleteOrgNode(Long id) { - // 校验存在 + // 1) 校验节点存在 validateOrgNodeExists(id); - // 删除 + + // 2) 校验是否有子节点 + if (orgNodeMapper.existsByParentId(id)) { + throw exception(ORG_NODE_DELETE_HAS_CHILDREN); + } + + // 3) 校验是否挂设备 + if (deviceMapper.existsByOrgNodeId(id)) { + throw exception(ORG_NODE_DELETE_HAS_DEVICES); + } + + // 4) 允许删除 orgNodeMapper.deleteById(id); } + @Override public void deleteOrgNodeListByIds(List ids) { // 删除 @@ -83,40 +130,239 @@ public class OrgNodeServiceImpl implements OrgNodeService { return orgNodeMapper.selectPage(pageReqVO); } - @Override public List getOrgTree(Long customerId) { - Long tenantId = TenantContextHolder.getRequiredTenantId(); - List list = orgNodeMapper.selectListByTenantAndCustomer(tenantId, customerId); - if (list.isEmpty()) { - return Collections.emptyList(); + // 1) customerId 可空:为空查全部客户;不为空查单个客户 + List customers; + if (customerId == null) { + customers = cusManagementMapper.selectList(); + } else { + ManagementDO customer = cusManagementMapper.selectById(customerId); + if (customer == null) { + return Collections.emptyList(); + } + customers = Collections.singletonList(customer); } - Map map = new HashMap<>(list.size()); - for (OrgNodeDO item : list) { - OrgNodeTreeRespVO vo = new OrgNodeTreeRespVO(); - vo.setId(item.getId()); - vo.setParentId(item.getParentId()); - vo.setNodeType(item.getNodeType()); // 1客户 2车间 3产线 - vo.setName(item.getName()); - vo.setChildren(new ArrayList<>()); - map.put(item.getId(), vo); + if (customers.isEmpty()) { + return Collections.emptyList(); } - List roots = new ArrayList<>(); - for (OrgNodeDO item : list) { - OrgNodeTreeRespVO current = map.get(item.getId()); - if (item.getParentId() == 0) { - roots.add(current); - } else { - OrgNodeTreeRespVO parent = map.get(item.getParentId()); - if (parent != null) { - parent.getChildren().add(current); + List roots = new ArrayList<>(customers.size()); + + // 2) 每个客户组一棵树 + for (ManagementDO customer : customers) { + Long cid = customer.getId(); + + // 客户根 + OrgNodeTreeRespVO customerRoot = new OrgNodeTreeRespVO(); + customerRoot.setId(cid); + customerRoot.setParentId(0L); + customerRoot.setNodeType(1); // 客户 + customerRoot.setName(customer.getCustomerName()); + customerRoot.setOperateStatus(null); + customerRoot.setChildren(new ArrayList<>()); + customerRoot.setNodeKey("C_" + cid); + customerRoot.setParentKey("0"); + + // 2.1 查组织节点(按 customer_id) + List orgList = orgNodeMapper.selectListByCustomer(cid); + Map orgMap = new HashMap<>(orgList.size()); + + for (OrgNodeDO item : orgList) { + OrgNodeTreeRespVO vo = new OrgNodeTreeRespVO(); + vo.setId(item.getId()); + vo.setParentId(item.getParentId()); + vo.setNodeType(item.getNodeType()); // 2车间 3产线 + vo.setName(item.getName()); + vo.setOperateStatus(null); + vo.setChildren(new ArrayList<>()); + vo.setNodeKey("O_" + item.getId()); + orgMap.put(item.getId(), vo); + } + + // 2.2 组装组织树:parentId=0 挂到客户根 + for (OrgNodeDO item : orgList) { + OrgNodeTreeRespVO current = orgMap.get(item.getId()); + if (Objects.equals(item.getParentId(), 0L)) { + current.setParentKey(customerRoot.getNodeKey()); // C_xxx + customerRoot.getChildren().add(current); + } else { + OrgNodeTreeRespVO parent = orgMap.get(item.getParentId()); + if (parent != null) { + current.setParentKey(parent.getNodeKey()); // O_xxx + parent.getChildren().add(current); + } else { + // 脏数据兜底:挂客户根 + current.setParentKey(customerRoot.getNodeKey()); + customerRoot.getChildren().add(current); + } } } + + // 2.3 挂设备节点 + operateStatus + // 这里请确保你的 SQL 能查到 org_node_id IS NULL / 0 的设备 + List devices = deviceMapper.selectByCustomerAndOrgNodeIds( + cid, + orgList.stream().map(OrgNodeDO::getId).collect(Collectors.toList()) + ); + + if (!devices.isEmpty()) { + List deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + Map statusMap = tDengineService.getLatestDeviceStatusAlternative(deviceIds); + + for (DeviceDO device : devices) { + Long orgNodeId = device.getOrgNodeId(); + + OrgNodeTreeRespVO deviceVo = new OrgNodeTreeRespVO(); + deviceVo.setId(device.getId()); + deviceVo.setParentId(orgNodeId == null ? 0L : orgNodeId); + deviceVo.setNodeType(4); // 设备 + deviceVo.setName(device.getDeviceName()); + deviceVo.setDeviceCode(device.getDeviceCode()); // 新增 + deviceVo.setOperateStatus(statusMap.get(device.getId())); + deviceVo.setChildren(Collections.emptyList()); + deviceVo.setNodeKey("D_" + device.getId()); + + // org_node_id 为空/0:直接挂客户根 + if (orgNodeId == null || orgNodeId == 0L) { + deviceVo.setParentKey(customerRoot.getNodeKey()); // C_xxx + customerRoot.getChildren().add(deviceVo); + continue; + } + + // 否则挂组织节点 + OrgNodeTreeRespVO parent = orgMap.get(orgNodeId); + if (parent != null) { + deviceVo.setParentKey(parent.getNodeKey()); // O_xxx + parent.getChildren().add(deviceVo); + } else { + // 脏数据兜底:挂客户根 + deviceVo.setParentKey(customerRoot.getNodeKey()); + customerRoot.getChildren().add(deviceVo); + } + } + } + + roots.add(customerRoot); } + return roots; } + @Override + public List getDeviceListByNode(Long nodeId, Integer nodeType) { + if (nodeId == null || nodeType == null) { + return Collections.emptyList(); + } + + List devices = Collections.emptyList(); + + // 4=设备:直接返回自己 + if (Objects.equals(nodeType, 4)) { + DeviceDO device = deviceMapper.selectById(nodeId); + if (device == null || Boolean.TRUE.equals(device.getDeleted())) { + return Collections.emptyList(); + } + devices = Collections.singletonList(device); + } + + // 1=客户:返回该客户全部设备 + else if (Objects.equals(nodeType, 1)) { + devices = deviceMapper.selectByCustomerId(nodeId); + } + + // 2/3=组织:返回当前节点及其子节点下全部设备 + else if (Objects.equals(nodeType, 2) || Objects.equals(nodeType, 3)) { + List all = orgNodeMapper.selectListByCustomerByNodeId(nodeId); + if (CollUtil.isEmpty(all)) { + return Collections.emptyList(); + } + + // parentId -> childrenIds + Map> childrenMap = new HashMap<>(); + for (OrgNodeDO n : all) { + childrenMap.computeIfAbsent(n.getParentId(), k -> new ArrayList<>()).add(n.getId()); + } + + // BFS 收集子树(包含自己) + Set subtreeIds = new HashSet<>(); + Deque queue = new ArrayDeque<>(); + queue.add(nodeId); + while (!queue.isEmpty()) { + Long currentId = queue.poll(); + if (!subtreeIds.add(currentId)) { + continue; + } + List children = childrenMap.get(currentId); + if (CollUtil.isNotEmpty(children)) { + queue.addAll(children); + } + } + + if (CollUtil.isEmpty(subtreeIds)) { + return Collections.emptyList(); + } + devices = deviceMapper.selectByOrgNodeIds(new ArrayList<>(subtreeIds)); + } + + // 其他 nodeType + else { + return Collections.emptyList(); + } + + if (CollUtil.isEmpty(devices)) { + return Collections.emptyList(); + } + + // 批量查运行状态(避免在 toSimpleVO 里逐条查) + List deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList()); + List ruleCodes = Arrays.stream(DeviceStatusEnum.values()) + .map(DeviceStatusEnum::getCode) + .collect(Collectors.toList()); + + List operationRecords = + tDengineService.selectLatestByDeviceAndRuleMinimal(deviceIds, ruleCodes); + + Map latestRecordMap = operationRecords.stream() + .collect(Collectors.toMap( + DeviceOperationRecordDO::getDeviceId, + r -> r, + (r1, r2) -> r1.getCreateTime().isAfter(r2.getCreateTime()) ? r1 : r2 + )); + + //vo转换 + + return getDeviceSimpleRespVOS(devices, latestRecordMap); + } + + + private static List getDeviceSimpleRespVOS(List devices, Map latestRecordMap) { + // 转 VO + List result = new ArrayList<>(devices.size()); + for (DeviceDO d : devices) { + DeviceSimpleRespVO vo = new DeviceSimpleRespVO(); + vo.setId(d.getId()); + vo.setDeviceCode(d.getDeviceCode()); + vo.setDeviceName(d.getDeviceName()); + vo.setCustomerId(d.getCustomerId()); + vo.setOrgNodeId(d.getOrgNodeId()); + vo.setStatus(d.getStatus()); + vo.setProtocol(d.getProtocol()); + vo.setIsEnable(d.getIsEnable()); + + DeviceOperationRecordDO record = latestRecordMap.get(d.getId()); + if (record != null) { + DeviceStatusEnum statusEnum = DeviceStatusEnum.getByCode(record.getRule()); + vo.setOperatingStatus(statusEnum != null ? statusEnum.getName() : DeviceStatusEnum.OFFLINE.getName()); + } else { + vo.setOperatingStatus(DeviceStatusEnum.OFFLINE.getName()); + } + + result.add(vo); + } + return result; + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/DeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/DeviceMapper.xml index f714ceb..4c0002a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/DeviceMapper.xml +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/DeviceMapper.xml @@ -200,8 +200,68 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/orgnode/OrgNodeMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/orgnode/OrgNodeMapper.xml index 3380cdc..de7292b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/orgnode/OrgNodeMapper.xml +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/orgnode/OrgNodeMapper.xml @@ -21,4 +21,34 @@ + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDeviceMapper.xml new file mode 100644 index 0000000..5ddc805 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDeviceMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDevicePropertyMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDevicePropertyMapper.xml deleted file mode 100644 index 47664f0..0000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/tdengine/IotDevicePropertyMapper.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 5bb08a4..f790b72 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -48,31 +48,31 @@ spring: datasource: master: name: besure-digital-center - url: jdbc:mysql://192.168.5.5:3307/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://ngsk.tech:3307/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root password: ngsk0809 driver-class-name: com.mysql.cj.jdbc.Driver -# -# tdengine: -# name: tdengine -# url: jdbc:TAOS-RS://ngsk.tech:16042/besure_server?charset=UTF-8&locale=en_US.UTF-8 -# username: root -# password: taosdata -# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver -# druid: # TDengine专用配置 -# initial-size: 1 -# max-active: 5 # TDengine建议较小的连接池 -# min-idle: 1 -# max-wait: 30000 # 缩短等待时间 -# time-between-eviction-runs-millis: 60000 -# min-evictable-idle-time-millis: 300000 -# validation-query: SELECT 1 -# test-while-idle: true -# test-on-borrow: false -# test-on-return: false -# pool-prepared-statements: false # TDengine REST驱动不支持预处理语句 -# max-pool-prepared-statement-per-connection-size: -1 -# connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000 + + tdengine: + name: tdengine + url: jdbc:TAOS-RS://192.168.5.119:6042/besure_server?charset=UTF-8&locale=en_US.UTF-8 + username: root + password: taosdata + driver-class-name: com.taosdata.jdbc.rs.RestfulDriver + druid: # TDengine专用配置 + initial-size: 1 + max-active: 5 # TDengine建议较小的连接池 + min-idle: 1 + max-wait: 30000 # 缩短等待时间 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: SELECT 1 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: false # TDengine REST驱动不支持预处理语句 + max-pool-prepared-statement-per-connection-size: -1 + connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: @@ -86,7 +86,7 @@ spring: # Quartz 配置项,对应 QuartzProperties 配置类 spring: quartz: - auto-startup: false # 测试环境,需要开启 Job + auto-startup: true # 测试环境,需要开启 Job scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true