From ced8e9c2f5f35792d3f7f7e665968a695f3536b7 Mon Sep 17 00:00:00 2001 From: HuangHuiKang Date: Mon, 23 Mar 2026 17:09:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E8=BF=81=E7=A7=BB=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=A1=A8=E5=92=8C=E5=91=8A=E8=AD=A6=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=A1=A8=E5=88=B0tdengine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceOperationRecordDO.java | 5 + .../iot/dal/mysql/device/DeviceMapper.java | 4 + .../DeviceOperationRecordMapper.java | 16 + .../mqtt/consumer/MqttDataHandler.java | 3 +- .../yudao/module/iot/job/DeviceJob.java | 14 +- .../iot/service/device/DeviceServiceImpl.java | 52 +- .../iot/service/device/TDengineService.java | 614 +++++++++++++++++- .../DeviceOperationRecordServiceImpl.java | 172 ++++- .../DeviceWarinningRecordServiceImpl.java | 22 +- .../resources/mapper/device/DeviceMapper.xml | 11 + .../DeviceOperationRecordMapper.xml | 76 +++ 11 files changed, 960 insertions(+), 29 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/deviceoperationrecord/DeviceOperationRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/deviceoperationrecord/DeviceOperationRecordDO.java index 22536c09c..856b7082c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/deviceoperationrecord/DeviceOperationRecordDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/deviceoperationrecord/DeviceOperationRecordDO.java @@ -73,4 +73,9 @@ public class DeviceOperationRecordDO extends BaseDO { */ private Double totalWarningTime; + /** + * 租户id + */ + private String tenantId; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/DeviceMapper.java index 8d6f27975..8cf745cb2 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 @@ -123,4 +123,8 @@ public interface DeviceMapper extends BaseMapperX { List selectLineBatch(@Param("deviceIds") List deviceIds); List selectDeviceIdsByLine(@Param("lineNode") String lineNode, @Param("lineName") String lineName); + + Integer getTotalDeviceCount(); + + List getAllDeviceIds(); } \ 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/deviceoperationrecord/DeviceOperationRecordMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/deviceoperationrecord/DeviceOperationRecordMapper.java index 52290e421..725dbe509 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/deviceoperationrecord/DeviceOperationRecordMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/deviceoperationrecord/DeviceOperationRecordMapper.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord; +import java.time.LocalDateTime; import java.util.*; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO; +import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; @@ -41,4 +43,18 @@ public interface DeviceOperationRecordMapper extends BaseMapperX selectLine(); List deviceOperationList(@Param("deviceTotalTimeRecordReqVO") DeviceTotalTimeRecordReqVO deviceTotalTimeRecordReqVO); + + IPage selectDevicePageFromMySQL( + Page page, + @Param("pageReqVO") DeviceTotalTimeRecordReqVO pageReqVO + ); + + @DS("tdengine") + List> selectDeviceStatsFromTD( + @Param("deviceIds") List deviceIds, + @Param("startTime") String startTime, + @Param("endTime") String endTime + ); + + List selectDeviceListFromMySQL(@Param("deviceTotalTimeRecordReqVO") DeviceTotalTimeRecordReqVO 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/framework/mqtt/consumer/MqttDataHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java index 193f4fe70..969ce401f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java @@ -514,7 +514,8 @@ public class MqttDataHandler extends SuperConsumer { //TODO 创建人和更新人为内置默认管理员 deviceWarinningRecordDO.setCreator("1"); deviceWarinningRecordDO.setUpdater("1"); - deviceWarinningRecordMapper.insert(deviceWarinningRecordDO); +// deviceWarinningRecordMapper.insert(deviceWarinningRecordDO); + tDengineService.insertDeviceWarningRecord(deviceWarinningRecordDO); } } 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 index 400cb11b9..043c21262 100644 --- 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 @@ -31,8 +31,6 @@ import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; - @Slf4j @Component public class DeviceJob implements JobHandler { @@ -167,7 +165,9 @@ public class DeviceJob implements JobHandler { record.setRule(DeviceStatusEnum.STANDBY.getCode()); record.setCreator("1"); record.setUpdater("1"); - deviceOperationRecordMapper.insert(record); +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); + return; } @@ -223,7 +223,9 @@ public class DeviceJob implements JobHandler { record.setCreator("1"); record.setUpdater("1"); - deviceOperationRecordMapper.insert(record); +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); + break; } } @@ -235,8 +237,8 @@ public class DeviceJob implements JobHandler { record.setRule(DeviceStatusEnum.OFFLINE.getCode()); record.setCreator("1"); record.setUpdater("1"); - deviceOperationRecordMapper.insert(record); - +// deviceOperationRecordMapper.insert(record); + tDengineService.insertDeviceOperationRecord(record); } /** 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 c409876c4..56cccc60e 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 @@ -332,7 +332,7 @@ public class DeviceServiceImpl implements DeviceService { // .orderByDesc(DeviceOperationRecordDO::getCreateTime) // ); - List operationRecords = deviceOperationRecordMapper.selectLatestByDeviceAndRule(deviceIds,ruleCodes); + List operationRecords = tdengineService.selectLatestByDeviceAndRuleMinimal(deviceIds,ruleCodes); // 按 deviceId 分组,取最新一条 Map latestRecordMap = operationRecords.stream() @@ -1172,15 +1172,57 @@ public class DeviceServiceImpl implements DeviceService { @Override public DeviceOperationStatusRespVO getDeviceOperationalStatus() { - DeviceOperationStatusRespVO deviceOperationalStatus= deviceMapper.getDeviceOperationalStatus(); + DeviceOperationStatusRespVO result = new DeviceOperationStatusRespVO(); + + // 1. 从 MySQL 获取设备总数 + Integer totalDevices = deviceMapper.getTotalDeviceCount(); + result.setTotalDevices(totalDevices); + + // 2. 从 MySQL 获取所有设备ID + List deviceIds = deviceMapper.getAllDeviceIds(); + + if (CollectionUtils.isEmpty(deviceIds)) { + // 没有设备,所有状态设为0 + result.setRunningCount(0); + result.setStandbyCount(0); + result.setFaultCount(0); + result.setWarningCount(0); + return result; + } + + // 3. 从 TDengine 批量查询设备最新状态 + Map deviceStatusMap = tdengineService.getLatestDeviceStatusAlternative(deviceIds); + + // 4. 统计各状态数量 + int runningCount = 0; + int standbyCount = 0; + int faultCount = 0; + int warningCount = 0; + + for (String status : deviceStatusMap.values()) { + switch (status) { + case "1": runningCount++; break; + case "2": standbyCount++; break; + case "3": faultCount++; break; + case "4": warningCount++; break; + default: break; + } + } + + result.setRunningCount(runningCount); + result.setStandbyCount(standbyCount); + result.setFaultCount(faultCount); + result.setWarningCount(warningCount); + + // 计算利用率 - calculateUtilizationRate(deviceOperationalStatus); + calculateUtilizationRate(result); // 计算故障率 - calculateFaultRate(deviceOperationalStatus); + calculateFaultRate(result); - return deviceOperationalStatus; + return result; } 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 45931e019..9ead153e6 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 @@ -1,8 +1,13 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.iocoder.yudao.framework.common.pojo.DeviceEdgeData; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.iot.controller.admin.device.enums.JavaToTdengineTypeEnum; +import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceTotalTimeRecordRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO; 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.devicewarinningrecord.DeviceWarinningRecordDO; import com.alibaba.fastjson.JSON; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; @@ -1564,4 +1569,611 @@ public class TDengineService { } -} \ No newline at end of file + // ========================== 运行记录表 ========================== + /** + * 插入设备运行记录到 TDengine + * @param record 设备运行记录 + * @return 是否插入成功 + */ + @DS("tdengine") + public boolean insertDeviceOperationRecord(DeviceOperationRecordDO record) { + + if (record == null || record.getDeviceId() == null) { + log.warn("设备运行记录参数为空"); + return false; + } + + // 表名 - 使用固定表名或按设备分表 + String tableName = "besure_server.iot_device_operation_record"; + // 或者按设备分表: "besure_server.operation_record_" + record.getDeviceId(); + + // 构建 SQL + StringBuilder columnBuilder = new StringBuilder(); + StringBuilder valueBuilder = new StringBuilder(); + List params = new ArrayList<>(); + + // 1. 必须字段: 时间戳 (TDengine 要求) + columnBuilder.append("ts"); + valueBuilder.append("?"); + // 使用当前时间作为 ts,或者使用 record 中的时间 +// if (record.getCreateTime() != null) { + // 将 LocalDateTime 转换为 Timestamp +// Timestamp tsTimestamp = Timestamp.valueOf(record.getCreateTime()); + params.add(new Timestamp(System.currentTimeMillis())); +// } else { +// params.add(new Timestamp(System.currentTimeMillis())); +// } + + // 2. 添加其他字段 +// addFieldIfNotNull(record.getId(), "id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getDeviceId(), "device_id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getModelId(), "model_id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getRule(), "rule", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getAddressValue(), "address_value", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getCreator(), "creator", columnBuilder, valueBuilder, params); + + // create_time 单独处理(如果已经作为 ts 使用,可以选择不重复插入) + + columnBuilder.append(", create_time"); + valueBuilder.append(", ?"); +// // 将 LocalDateTime 转换为 Timestamp +// LocalDateTime createTime = record.getCreateTime(); +// Timestamp timestamp = Timestamp.valueOf(createTime); + params.add(new Timestamp(System.currentTimeMillis())); + + + addFieldIfNotNull(record.getUpdater(), "updater", columnBuilder, valueBuilder, params); + + // update_time + + columnBuilder.append(", update_time"); + valueBuilder.append(", ?"); + params.add(new Timestamp(System.currentTimeMillis())); + + + addFieldIfNotNull(0,"deleted", columnBuilder, valueBuilder, params); + //TODO 待优化租户ID + addFieldIfNotNull("1", "tenant_id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(record.getRuleId(), "rule_id", columnBuilder, valueBuilder, params); + + // 构建完整 SQL + String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", + tableName, columnBuilder.toString(), valueBuilder.toString()); + + try { + int rows = jdbcTemplate.update(sql, params.toArray()); + log.info("TDengine 设备运行记录插入成功: deviceId={}, table={}, rows={}", + record.getDeviceId(), tableName, rows); + return rows > 0; + } catch (Exception e) { + log.error("TDengine 设备运行记录插入失败: deviceId={}, table={}, sql={}", + record.getDeviceId(), tableName, sql, e); + return false; + } + } + + /** + * 辅助方法:添加字段到 SQL + */ + private void addFieldIfNotNull(Object value, String columnName, + StringBuilder columnBuilder, StringBuilder valueBuilder, + List params) { + if (value != null) { + if (columnBuilder.length() > 0 && !columnBuilder.toString().endsWith("ts")) { + columnBuilder.append(", "); + valueBuilder.append(", "); + } else if (columnBuilder.length() > 0) { + columnBuilder.append(", "); + valueBuilder.append(", "); + } + columnBuilder.append(columnName); + valueBuilder.append("?"); + params.add(value); + } + } + + + /** + * 批量查询设备最新一条运行记录 + */ + @DS("tdengine") + public List selectLatestByDeviceAndRuleMinimal( + List deviceIds, + List ruleCodes) { + + if (CollectionUtils.isEmpty(deviceIds)) { + return Collections.emptyList(); + } + + List params = new ArrayList<>(); + StringBuilder sql = new StringBuilder( + "SELECT * FROM besure_server.iot_device_operation_record " + + "WHERE deleted = 0 AND device_id IN (" + ); + + for (int i = 0; i < deviceIds.size(); i++) { + if (i > 0) sql.append(", "); + sql.append("?"); + params.add(deviceIds.get(i)); + } + sql.append(") "); + + if (CollectionUtils.isNotEmpty(ruleCodes)) { + sql.append("AND rule IN ("); + for (int i = 0; i < ruleCodes.size(); i++) { + if (i > 0) sql.append(", "); + sql.append("?"); + params.add(ruleCodes.get(i)); + } + sql.append(") "); + } + + sql.append("ORDER BY create_time DESC"); + + try { + return jdbcTemplate.query(sql.toString(), params.toArray(), (rs, rowNum) -> { + DeviceOperationRecordDO d = new DeviceOperationRecordDO(); +// d.setTs(rs.getTimestamp("ts") != null ? rs.getTimestamp("ts").toLocalDateTime() : null); +// d.setId(rs.getLong("id")); + d.setDeviceId(rs.getLong("device_id")); + d.setModelId(rs.getLong("model_id")); + d.setRule(rs.getString("rule")); + d.setAddressValue(rs.getString("address_value")); + d.setCreator(rs.getString("creator")); + d.setCreateTime(rs.getTimestamp("create_time") != null ? rs.getTimestamp("create_time").toLocalDateTime() : null); + d.setUpdater(rs.getString("updater")); + d.setUpdateTime(rs.getTimestamp("update_time") != null ? rs.getTimestamp("update_time").toLocalDateTime() : null); + d.setDeleted(rs.getBoolean("deleted")); + d.setTenantId(String.valueOf(rs.getLong("tenant_id"))); + d.setRuleId(rs.getLong("rule_id")); + return d; + }); + + } catch (Exception e) { + log.error("查询失败", e); + return Collections.emptyList(); + } + } + + /** + * 从 TDengine 批量查询设备运行记录表最新状态 + */ + @DS("tdengine") + public Map getLatestDeviceStatusAlternative(List deviceIds) { + if (CollectionUtils.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + + Map statusMap = new HashMap<>(); + + // 分批查询,每批50个设备 + int batchSize = 50; + for (int i = 0; i < deviceIds.size(); i += batchSize) { + int end = Math.min(i + batchSize, deviceIds.size()); + List batchIds = deviceIds.subList(i, end); + + // 为每批设备构建查询 + batchIds.forEach(deviceId -> { + String status = getSingleDeviceStatus(deviceId); + if (status != null) { + statusMap.put(deviceId, status); + } + }); + } + + return statusMap; + } + + /** + * 查询单个设备的最新状态 + */ + @DS("tdengine") + private String getSingleDeviceStatus(Long deviceId) { + String sql = "SELECT rule " + + "FROM besure_server.iot_device_operation_record " + + "WHERE device_id = ? " + + " AND deleted = 0 " + + " AND rule IN ('1', '2', '3', '4') " + + "ORDER BY create_time DESC " + + "LIMIT 1"; + + try { + return jdbcTemplate.queryForObject(sql, String.class, deviceId); + } catch (EmptyResultDataAccessException e) { + // 没有记录 + return null; + } catch (Exception e) { + log.error("查询设备 {} 状态失败: {}", deviceId, e.getMessage()); + return null; + } + } + + @DS("tdengine") + public Map getDeviceTimeStatsFromTD(List deviceIds, String startTime, String endTime) { + if (CollectionUtils.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + + StringBuilder sql = new StringBuilder(); + List params = new ArrayList<>(); + + sql.append("SELECT ") + .append(" device_id, ") + .append(" SUM(CASE WHEN rule = '0' THEN 1 ELSE 0 END) * 60 AS totalOfflineTime, ") + .append(" SUM(CASE WHEN rule = '1' THEN 1 ELSE 0 END) * 60 AS totalRunningTime, ") + .append(" SUM(CASE WHEN rule = '2' THEN 1 ELSE 0 END) * 60 AS totalStandbyTime, ") + .append(" SUM(CASE WHEN rule = '3' THEN 1 ELSE 0 END) * 60 AS totalFaultTime ") + .append("FROM besure_server.iot_device_operation_record ") + .append("WHERE deleted = 0 ") + .append(" AND device_id IN ("); + + for (int i = 0; i < deviceIds.size(); i++) { + if (i > 0) sql.append(", "); + sql.append("?"); + params.add(deviceIds.get(i)); + } + sql.append(") "); + + if (startTime != null) { + sql.append(" AND create_time >= ? "); + params.add(Timestamp.valueOf(startTime)); + } + + if (endTime != null) { + sql.append(" AND create_time <= ? "); + params.add(Timestamp.valueOf(endTime)); + } + + sql.append("GROUP BY device_id"); + + try { + List> result = jdbcTemplate.queryForList( + sql.toString(), params.toArray() + ); + + return convertStatsToMap(result); + + } catch (Exception e) { + log.error("TDengine 查询设备运行时间失败", e); + return Collections.emptyMap(); + } + } + + /** + * 转换统计结果 + */ + private Map convertStatsToMap( + List> result) { + + Map statsMap = new HashMap<>(); + + for (Map row : result) { + Long deviceId = ((Number) row.get("device_id")).longValue(); + + DeviceTotalTimeRecordRespVO vo = new DeviceTotalTimeRecordRespVO(); + vo.setId(deviceId); + vo.setTotalOfflineTime(getIntValue(row, "totalofflinetime")); + vo.setTotalRunningTime(getIntValue(row, "totalrunningtime")); + vo.setTotalStandbyTime(getIntValue(row, "totalstandbytime")); + vo.setTotalFaultTime(getIntValue(row, "totalfaulttime")); + + statsMap.put(deviceId, vo); + } + + return statsMap; + } + + /** + * 安全获取整数值 + */ + private Integer getIntValue(Map map, String key) { + Object value = map.get(key); + if (value == null) return 0; + if (value instanceof Number) return ((Number) value).intValue(); + return 0; + } + + + //=============================== 告警记录表 =============================== + /** + * 插入告警记录到 TDengine(使用已有的辅助方法) + */ + @DS("tdengine") + public boolean insertDeviceWarningRecord(DeviceWarinningRecordDO deviceWarningRecordDO) { + if (deviceWarningRecordDO == null) { + log.warn("告警记录参数为空"); + return false; + } + + String tableName = "besure_server.iot_device_warning_record"; + + StringBuilder columnBuilder = new StringBuilder(); + StringBuilder valueBuilder = new StringBuilder(); + List params = new ArrayList<>(); + + // 1. 必须字段: 时间戳 + columnBuilder.append("ts"); + valueBuilder.append("?"); + params.add(new Timestamp(System.currentTimeMillis())); + + // 2. 设备信息 +// addFieldIfNotNull(deviceWarningRecordDO.getId(), "id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getDeviceId(), "device_id", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getModelId(), "model_id", columnBuilder, valueBuilder, params); + + // 3. 规则和告警信息 + addFieldIfNotNull(deviceWarningRecordDO.getRule(), "rule", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getAlarmLevel(), "alarm_level", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getAddressValue(), "address_value", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getRuleId(), "rule_id", columnBuilder, valueBuilder, params); + + // 4. 名称信息 + addFieldIfNotNull(deviceWarningRecordDO.getDeviceName(), "device_name", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getModelName(), "model_name", columnBuilder, valueBuilder, params); + addFieldIfNotNull(deviceWarningRecordDO.getRuleName(), "rule_name", columnBuilder, valueBuilder, params); + + // 5. 创建者和更新者 + //TODO 待优化 + addFieldIfNotNull("1", "creator", columnBuilder, valueBuilder, params); + addFieldIfNotNull("1", "updater", columnBuilder, valueBuilder, params); + + // 6. 时间字段 + addFieldIfNotNull(new Timestamp(System.currentTimeMillis()), "create_time", columnBuilder, valueBuilder, params); + addFieldIfNotNull(new Timestamp(System.currentTimeMillis()), "update_time", columnBuilder, valueBuilder, params); + + // 7. 删除标志 + addFieldIfNotNull(0, "deleted", columnBuilder, valueBuilder, params); + + // 8. 租户ID + //TODO 待优化 + addFieldIfNotNull("1", "tenant_id", columnBuilder, valueBuilder, params); + + // 构建完整 SQL + String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", + tableName, columnBuilder.toString(), valueBuilder.toString()); + + try { + int rows = jdbcTemplate.update(sql, params.toArray()); + log.info("告警记录插入成功: deviceId={}, rule={}", + deviceWarningRecordDO.getDeviceId(), + deviceWarningRecordDO.getRule()); + return rows > 0; + } catch (Exception e) { + log.error("告警记录插入失败: deviceId={}, sql={}", + deviceWarningRecordDO.getDeviceId(), sql, e); + return false; + } + } + + + + /** + * TDengine 分页查询告警记录 + */ + @DS("tdengine") + public PageResult selectRunngingRecordPage(DeviceWarinningRecordPageReqVO reqVO) { + + // 构建查询 SQL + StringBuilder sql = new StringBuilder("SELECT * FROM besure_server.iot_device_warning_record "); + StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM besure_server.iot_device_warning_record "); + List params = new ArrayList<>(); + List countParams = new ArrayList<>(); + + // 添加 WHERE 条件 + String whereCondition = buildWhereCondition(reqVO, params); + String countWhereCondition = buildWhereCondition(reqVO, countParams); + + sql.append(whereCondition); + countSql.append(countWhereCondition); + + // 添加排序 + sql.append(" ORDER BY create_time DESC"); + + // 添加分页 + if (reqVO.getPageNo() != null && reqVO.getPageSize() != null) { + int offset = (reqVO.getPageNo() - 1) * reqVO.getPageSize(); + sql.append(" LIMIT ").append(reqVO.getPageSize()).append(" OFFSET ").append(offset); + } + + try { + // 1. 查询总数 + Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class, countParams.toArray()); + + if (total == null || total == 0) { + return new PageResult<>(Collections.emptyList(), 0L); + } + + // 2. 查询数据 + List records = jdbcTemplate.query( + sql.toString(), + params.toArray(), + (rs, rowNum) -> mapRowToDO(rs) + ); + + return new PageResult<>(records, total); + + } catch (Exception e) { + log.error("TDengine 分页查询告警记录失败", e); + return new PageResult<>(Collections.emptyList(), 0L); + } + } + + /** + * 构建 WHERE 条件 + */ + private String buildWhereCondition(DeviceWarinningRecordPageReqVO reqVO, List params) { + StringBuilder where = new StringBuilder(" WHERE deleted = 0 "); + + // 设备ID + if (reqVO.getDeviceId() != null) { + where.append(" AND device_id = ? "); + params.add(reqVO.getDeviceId()); + } + + // 模型ID + if (reqVO.getModelId() != null) { + where.append(" AND model_id = ? "); + params.add(reqVO.getModelId()); + } + + // 规则 + if (StringUtils.isNotBlank(reqVO.getRule())) { + where.append(" AND rule = ? "); + params.add(reqVO.getRule()); + } + + // 告警级别 + if (StringUtils.isNotBlank(reqVO.getAlarmLevel())) { + where.append(" AND alarm_level = ? "); + params.add(reqVO.getAlarmLevel()); + } + + // 地址值 + if (StringUtils.isNotBlank(reqVO.getAddressValue())) { + where.append(" AND address_value = ? "); + params.add(reqVO.getAddressValue()); + } + + // 创建时间范围 + if (reqVO.getCreateTime() != null && reqVO.getCreateTime().length == 2) { + LocalDateTime startTime = reqVO.getCreateTime()[0]; + LocalDateTime endTime = reqVO.getCreateTime()[1]; + + 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)); + } + } + + // 规则ID + if (reqVO.getRuleId() != null) { + where.append(" AND rule_id = ? "); + params.add(reqVO.getRuleId()); + } + + return where.toString(); + } + + /** + * 映射查询结果到 DO + */ + private DeviceWarinningRecordDO mapRowToDO(ResultSet rs) throws SQLException { + DeviceWarinningRecordDO record = new DeviceWarinningRecordDO(); + + // 时间戳 + Timestamp ts = rs.getTimestamp("ts"); +// if (ts != null) { +// record.setTs(ts.toLocalDateTime()); +// } + + // 其他字段 +// record.setId(rs.getLong("id")); + record.setDeviceId(rs.getLong("device_id")); + record.setModelId(rs.getLong("model_id")); + record.setRule(rs.getString("rule")); + record.setAlarmLevel(rs.getString("alarm_level")); + record.setAddressValue(rs.getString("address_value")); + record.setCreator(rs.getString("creator")); + + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + record.setCreateTime(createTime.toLocalDateTime()); + } + + record.setUpdater(rs.getString("updater")); + + Timestamp updateTime = rs.getTimestamp("update_time"); + if (updateTime != null) { + record.setUpdateTime(updateTime.toLocalDateTime()); + } + + record.setDeleted(rs.getInt("deleted") == 1); +// record.setTenantId(rs.getLong("tenant_id")); + record.setRuleId(rs.getLong("rule_id")); + record.setDeviceName(rs.getString("device_name")); + record.setModelName(rs.getString("model_name")); + record.setRuleName(rs.getString("rule_name")); + + return record; + } + + + + /** + * 查询设备告警记录列表(限制100条) + */ + @DS("tdengine") + public List selectDeviceWarningList(Long deviceId) { + + StringBuilder sql = new StringBuilder(); + List params = new ArrayList<>(); + + sql.append("SELECT * FROM besure_server.iot_device_warning_record ") + .append("WHERE deleted = 0 "); + + if (deviceId != null) { + sql.append("AND device_id = ? "); + params.add(deviceId); + } + + sql.append("ORDER BY create_time DESC ") + .append("LIMIT 100"); + + try { + return jdbcTemplate.query(sql.toString(), params.toArray(), (rs, rowNum) -> { + return mapRowToWarningRecordDO(rs); + }); + } catch (Exception e) { + log.error("查询设备告警记录失败: deviceId={}", deviceId, e); + return Collections.emptyList(); + } + } + + /** + * 映射查询结果 + */ + private DeviceWarinningRecordDO mapRowToWarningRecordDO(ResultSet rs) throws SQLException { + DeviceWarinningRecordDO record = new DeviceWarinningRecordDO(); + + // 时间戳 + Timestamp ts = rs.getTimestamp("ts"); +// if (ts != null) { +// record.setTs(ts.toLocalDateTime()); +// } + + // 其他字段 +// record.setId(rs.getLong("id")); + record.setDeviceId(rs.getLong("device_id")); + record.setModelId(rs.getLong("model_id")); + record.setRule(rs.getString("rule")); + record.setAlarmLevel(rs.getString("alarm_level")); + record.setAddressValue(rs.getString("address_value")); + record.setCreator(rs.getString("creator")); + + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + record.setCreateTime(createTime.toLocalDateTime()); + } + + record.setUpdater(rs.getString("updater")); + + Timestamp updateTime = rs.getTimestamp("update_time"); + if (updateTime != null) { + record.setUpdateTime(updateTime.toLocalDateTime()); + } + + record.setDeleted(rs.getInt("deleted") == 1); +// record.setTenantId(rs.getLong("tenant_id")); + record.setRuleId(rs.getLong("rule_id")); + record.setDeviceName(rs.getString("device_name")); + record.setModelName(rs.getString("model_name")); + record.setRuleName(rs.getString("rule_name")); + + return record; + } + } \ 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/deviceoperationrecord/DeviceOperationRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java index 0ebf99b5c..c388d2c1a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.service.deviceoperationrecord; import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.utils.TimeConverterUtil; +import cn.iocoder.yudao.module.iot.service.device.TDengineService; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -40,6 +42,9 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe @Resource private DeviceOperationRecordMapper deviceOperationRecordMapper; + @Resource + private TDengineService tDengineService; + @Override public Long createDeviceOperationRecord(DeviceOperationRecordSaveReqVO createReqVO) { @@ -86,15 +91,56 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe @Override public PageResult deviceOperationPage(DeviceTotalTimeRecordReqVO pageReqVO) { + // 1. 先从 MySQL 查询设备分页信息 Page page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); - IPage deviceOperationRecordRespVOIPage = deviceOperationRecordMapper.deviceOperationPage(page,pageReqVO); - // 计算和转换 - if (deviceOperationRecordRespVOIPage != null && !deviceOperationRecordRespVOIPage.getRecords().isEmpty()) { - calculateAndSetConvertedValues(deviceOperationRecordRespVOIPage.getRecords(),pageReqVO); + IPage mysqlDevicePage = + deviceOperationRecordMapper.selectDevicePageFromMySQL(page, pageReqVO); + + if (mysqlDevicePage == null || mysqlDevicePage.getRecords().isEmpty()) { + return new PageResult<>(Collections.emptyList(), 0L); + } + + // 2. 提取设备ID列表 + List deviceIds = mysqlDevicePage.getRecords().stream() + .map(DeviceTotalTimeRecordRespVO::getId) + .collect(Collectors.toList()); + + // 3. 从 TDengine 批量查询统计数据 + List> statsList = + deviceOperationRecordMapper.selectDeviceStatsFromTD( + deviceIds, + pageReqVO.getStartTime(), + pageReqVO.getEndTime() + ); + + // 4. 转换为 Map<设备ID, 统计数据> + Map tdStatsMap = convertStatsListToMap(statsList); + + + // 5. 合并数据 + List result = new ArrayList<>(); + for (DeviceTotalTimeRecordRespVO device : mysqlDevicePage.getRecords()) { + DeviceTotalTimeRecordRespVO stats = tdStatsMap.get(device.getId()); + if (stats != null) { + // 设置统计数据 + device.setTotalOfflineTime(stats.getTotalOfflineTime()); + device.setTotalRunningTime(stats.getTotalRunningTime()); + device.setTotalStandbyTime(stats.getTotalStandbyTime()); + device.setTotalFaultTime(stats.getTotalFaultTime()); + } else { + // 没有统计数据,设为0 + device.setTotalOfflineTime(0); + device.setTotalRunningTime(0); + device.setTotalStandbyTime(0); + device.setTotalFaultTime(0); + } + result.add(device); } + // 5. 计算和转换 + calculateAndSetConvertedValues(result, pageReqVO); - return new PageResult<>(deviceOperationRecordRespVOIPage.getRecords(), deviceOperationRecordRespVOIPage.getTotal()); + return new PageResult<>(result, mysqlDevicePage.getTotal()); } @@ -118,11 +164,50 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe deviceTotalTimeRecordReqVO.setEndTime(LocalDateTime.now().format(formatter)); deviceTotalTimeRecordReqVO.setIds(ids); - // 4 查询统计数据 - List list = - deviceOperationRecordMapper.deviceOperationList(deviceTotalTimeRecordReqVO); +// // 4 查询统计数据 +// List list = +// deviceOperationRecordMapper.deviceOperationList(deviceTotalTimeRecordReqVO); - if (list != null && !list.isEmpty()) { + + // 4.1 先从 MySQL 查询设备列表 + List deviceList = + deviceOperationRecordMapper.selectDeviceListFromMySQL(deviceTotalTimeRecordReqVO); + + if (CollectionUtils.isEmpty(deviceList)) { + return Collections.emptyList(); + } + + // 4.2 提取设备ID列表 + List deviceIds = deviceList.stream() + .map(DeviceTotalTimeRecordRespVO::getId) + .collect(Collectors.toList()); + + // 4.3 从 TDengine 批量查询设备运行时间统计 + Map statsMap = + tDengineService.getDeviceTimeStatsFromTD(deviceIds, deviceTotalTimeRecordReqVO.getStartTime(), deviceTotalTimeRecordReqVO.getEndTime()); + + // 4.4 合并数据 + List list = new ArrayList<>(); + for (DeviceTotalTimeRecordRespVO device : deviceList) { + DeviceTotalTimeRecordRespVO stats = statsMap.get(device.getId()); + if (stats != null) { + // 设置统计数据 + device.setTotalOfflineTime(stats.getTotalOfflineTime()); + device.setTotalRunningTime(stats.getTotalRunningTime()); + device.setTotalStandbyTime(stats.getTotalStandbyTime()); + device.setTotalFaultTime(stats.getTotalFaultTime()); + } else { + // 没有统计数据,设为0 + device.setTotalOfflineTime(0); + device.setTotalRunningTime(0); + device.setTotalStandbyTime(0); + device.setTotalFaultTime(0); + } + list.add(device); + } + + + if (!list.isEmpty()) { // 5 计算转换 @@ -239,4 +324,73 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe record.setUtilizationRate("0%"); } + + /** + * 将统计列表转换为 Map<设备ID, 统计数据> + */ + private Map convertStatsListToMap(List> statsList) { + if (CollectionUtils.isEmpty(statsList)) { + return Collections.emptyMap(); + } + + Map statsMap = new HashMap<>(); + + for (Map stat : statsList) { + Object deviceIdObj = stat.get("deviceid"); + if (deviceIdObj == null) { + continue; + } + + Long deviceId = null; + if (deviceIdObj instanceof Number) { + deviceId = ((Number) deviceIdObj).longValue(); + } else if (deviceIdObj instanceof String) { + try { + deviceId = Long.parseLong((String) deviceIdObj); + } catch (NumberFormatException e) { + log.warn("设备ID格式错误: {}", deviceIdObj); + continue; + } + } else { + log.warn("设备ID类型错误: {}", deviceIdObj.getClass()); + continue; + } + + DeviceTotalTimeRecordRespVO vo = new DeviceTotalTimeRecordRespVO(); + vo.setId(deviceId); + + // 安全设置统计值 + vo.setTotalOfflineTime(getIntValue(stat, "totalofflinetime")); + vo.setTotalRunningTime(getIntValue(stat, "totalrunningtime")); + vo.setTotalStandbyTime(getIntValue(stat, "totalstandbytime")); + vo.setTotalFaultTime(getIntValue(stat, "totalfaulttime")); + + statsMap.put(deviceId, vo); + } + + return statsMap; + } + + /** + * 安全获取整数值 + */ + private Integer getIntValue(Map map, String key) { + Object value = map.get(key.toLowerCase()); + if (value == null) { + return 0; + } + + if (value instanceof Number) { + return ((Number) value).intValue(); + } else if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + log.warn("{} 值格式错误: {}", key, value); + return 0; + } + } + + return 0; + } } \ 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 c44ddb764..7de165238 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,5 +1,6 @@ package cn.iocoder.yudao.module.iot.service.devicewarinningrecord; +import cn.iocoder.yudao.module.iot.service.device.TDengineService; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -34,6 +35,10 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe @Resource private DeviceWarinningRecordMapper deviceWarinningRecordMapper; + @Resource + private TDengineService tDengineService; + + @Override public Long createDeviceWarinningRecord(DeviceWarinningRecordSaveReqVO createReqVO) { // 插入 @@ -73,7 +78,8 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe @Override public PageResult getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO) { - return deviceWarinningRecordMapper.selectPage(pageReqVO); +// return deviceWarinningRecordMapper.selectPage(pageReqVO); + return tDengineService.selectRunngingRecordPage(pageReqVO); } @Override @@ -81,12 +87,14 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe // 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 deviceWarinningRecordMapper.selectList( +// Wrappers.lambdaQuery() +// .eq(id != null, DeviceWarinningRecordDO::getDeviceId, id) +// .orderByDesc(DeviceWarinningRecordDO::getCreateTime) +// .last("LIMIT 100")// 限制 100 条 +// ); + + return tDengineService.selectDeviceWarningList(id); } @Override 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 c3e46238f..f714cebc2 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 @@ -192,5 +192,16 @@ AND mo.name LIKE CONCAT('%', #{lineName}, '%') + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/deviceoperationrecord/DeviceOperationRecordMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/deviceoperationrecord/DeviceOperationRecordMapper.xml index 0e1d481db..ebd400132 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/deviceoperationrecord/DeviceOperationRecordMapper.xml +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/deviceoperationrecord/DeviceOperationRecordMapper.xml @@ -157,4 +157,80 @@ GROUP BY ide.id, ide.device_code, ide.device_name ORDER BY ide.id desc + + + + + + + + + \ No newline at end of file