From 5778e728279b74e4816e7d0883fedbc507cc1209 Mon Sep 17 00:00:00 2001 From: HuangHuiKang Date: Mon, 2 Mar 2026 17:00:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=B7=B2=E7=9F=A5?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/device/DeviceController.java | 19 +- .../device/vo/LineCodeAndNameRespVO.java | 13 + .../admin/device/vo/LineDeviceRespVO.java | 2 +- .../vo/DeviceTotalTimeRecordRespVO.java | 26 +- .../iot/dal/dataobject/device/DeviceDO.java | 1 + .../iot/dal/mysql/device/DeviceMapper.java | 13 +- .../mqtt/consumer/MqttDataHandler.java | 82 ++-- .../yudao/module/iot/job/DeviceJob.java | 393 ++++++++++++++++-- .../iot/service/device/DeviceServiceImpl.java | 46 +- .../iot/service/device/TDengineService.java | 52 ++- .../DeviceOperationRecordServiceImpl.java | 94 +++-- .../resources/mapper/device/DeviceMapper.xml | 30 ++ .../DeviceOperationRecordMapper.xml | 28 +- .../admin/dashboard/DashboardController.java | 16 +- .../organization/OrganizationServiceImpl.java | 65 ++- 15 files changed, 707 insertions(+), 173 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineCodeAndNameRespVO.java 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 fb339e746..d0b401506 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 @@ -121,7 +121,7 @@ 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(); @@ -199,17 +199,20 @@ public class DeviceController { @ApiAccessLog(operateType = EXPORT) public void exportLineDevice(@Valid LineDeviceRequestVO pageReqVO, HttpServletResponse response) throws IOException { - List lineDeviceList = deviceService.lineDeviceList(pageReqVO); + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + + + PageResult lineDeviceRespVOPageResult = deviceService.lineDevicePage(pageReqVO); // 设置响应头 - response.setContentType("application/vnd.ms-excel;charset=UTF-8"); - response.setHeader("Content-Disposition", - "attachment;filename=" + URLEncoder.encode("设备运行报表记录.xls", "UTF-8")); - response.setHeader("Content-Encoding", "identity"); +// response.setContentType("application/vnd.ms-excel;charset=UTF-8"); +// response.setHeader("Content-Disposition", +// "attachment;filename=" + URLEncoder.encode("设备运行报表记录.xls", "UTF-8")); +// response.setHeader("Content-Encoding", "identity"); // 导出Excel String fileName = String.format("数据实时监控_%s.xls", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))); // 导出 Excel - ExcelUtils.write(response, fileName, "数据", LineDeviceRespVO.class,lineDeviceList); + ExcelUtils.write(response, fileName, "数据", LineDeviceRespVO.class,lineDeviceRespVOPageResult.getList()); } @@ -238,7 +241,7 @@ public class DeviceController { @GetMapping("/getDeviceOperationalStatus") @Operation(summary = "获取首页设备运行状态") - @PreAuthorize("@ss.hasPermission('iot:device:query')") +// @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(); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineCodeAndNameRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineCodeAndNameRespVO.java new file mode 100644 index 000000000..09f9f91a5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineCodeAndNameRespVO.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo; + +import lombok.Data; + +@Data +public class LineCodeAndNameRespVO { + + private Long deviceId; + + private String lineCode; + + private String lineName; +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineDeviceRespVO.java index d48f95632..e95940d03 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/LineDeviceRespVO.java @@ -39,7 +39,7 @@ public class LineDeviceRespVO { private String deviceName; @Schema(description = "状态 1-在线 2-离线") - @ExcelProperty(value = "连接状态", converter = DictConvert.class) +// @ExcelProperty(value = "连接状态", converter = DictConvert.class) @DictFormat("iot_gateway_status") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 private String status; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/vo/DeviceTotalTimeRecordRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/vo/DeviceTotalTimeRecordRespVO.java index bdb6da6a7..40c8584a4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/vo/DeviceTotalTimeRecordRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/vo/DeviceTotalTimeRecordRespVO.java @@ -21,23 +21,31 @@ public class DeviceTotalTimeRecordRespVO { @Schema(description = "设备名称") @ExcelProperty("设备名称") private String deviceName; - - @Schema(description = "运行时间(小时)") - @ExcelProperty("运行时间(小时)") + + @Schema(description = "离线时间(小时)") +// @ExcelProperty("离线时间(s)") + private double totalOfflineTime; + + @Schema(description = "运行时间(s)") + @ExcelProperty("运行时间(s)") private double totalRunningTime; - @Schema(description = "待机时间(小时)") - @ExcelProperty("待机时间(小时)") + @Schema(description = "待机时间(s)") + @ExcelProperty("待机时间(s)") private double totalStandbyTime; - @Schema(description = "故障时间(小时)") - @ExcelProperty("故障时间(小时)") + @Schema(description = "故障时间(s)") + @ExcelProperty("故障时间(s)") private double totalFaultTime; - @Schema(description = "警告时间(小时)") - @ExcelProperty("警告时间(小时)") + @Schema(description = "警告时间(s)") +// @ExcelProperty("警告时间(s)") private double totalWarningTime; + @Schema(description = "开机率") + @ExcelProperty("开机率") + private String powerOnRate; + @Schema(description = "稼动率") @ExcelProperty("稼动率") private String utilizationRate; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/DeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/DeviceDO.java index 52668bca1..51a283c07 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/DeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/DeviceDO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; 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 e5f013f96..8d6f27975 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 @@ -4,15 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceOperationStatusRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRequestVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import com.alibaba.excel.util.StringUtils; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; @@ -47,7 +43,7 @@ public interface DeviceMapper extends BaseMapperX { .eqIfPresent(DeviceDO::getRemark, reqVO.getRemark()) .eqIfPresent(DeviceDO::getIsEnable, reqVO.getIsEnable()) .betweenIfPresent(DeviceDO::getCreateTime, reqVO.getCreateTime()) - .orderByDesc(DeviceDO::getCreateTime); + .orderByDesc(DeviceDO::getId); // 单独处理 ids 条件 if (StringUtils.isNotBlank(reqVO.getIds())) { @@ -122,4 +118,9 @@ public interface DeviceMapper extends BaseMapperX { */ @Select("SELECT device_ids FROM mes_goview WHERE id = #{goviewId}") String selectDeviceIdsByGoviewId(@Param("goviewId") Long goviewId); + + + List selectLineBatch(@Param("deviceIds") List deviceIds); + + List selectDeviceIdsByLine(@Param("lineNode") String lineNode, @Param("lineName") String lineName); } \ 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 0fff28503..193f4fe70 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 @@ -204,7 +204,6 @@ public class MqttDataHandler extends SuperConsumer { } } - public void processDeviceDataFromMqtt(DeviceDO device, Map varListMap) { @@ -217,15 +216,15 @@ public class MqttDataHandler extends SuperConsumer { log.warn("设备 {} 未配置点位", device.getId()); - DeviceOperationRecordDO record = new DeviceOperationRecordDO(); - record.setDeviceId(deviceId); - record.setRule(DeviceStatusEnum.STANDBY.getCode()); - //TODO 待优化 - record.setTotalStandbyTime(device.getSampleCycle()); - record.setCreator("1"); - record.setUpdater("1"); - - deviceOperationRecordMapper.insert(record); +// DeviceOperationRecordDO record = new DeviceOperationRecordDO(); +// record.setDeviceId(deviceId); +// record.setRule(DeviceStatusEnum.STANDBY.getCode()); +// //TODO 待优化 +// record.setTotalStandbyTime(device.getSampleCycle()); +// record.setCreator("1"); +// record.setUpdater("1"); +// +// deviceOperationRecordMapper.insert(record); return; } @@ -236,22 +235,22 @@ public class MqttDataHandler extends SuperConsumer { return; } - // 查询RUNNING点位规则 - DevicePointRulesDO devicePoints = getDevicePointRules(deviceId); - if (StringUtils.isBlank(devicePoints.getFieldRule())){ - log.warn("设备 {} 没有RUNNING点位规则", device.getId()); - - DeviceOperationRecordDO record = new DeviceOperationRecordDO(); - record.setDeviceId(deviceId); - record.setRule(DeviceStatusEnum.STANDBY.getCode()); - //TODO 待优化 - record.setTotalStandbyTime(device.getSampleCycle()); - record.setCreator("1"); - record.setUpdater("1"); - - deviceOperationRecordMapper.insert(record); - - } + // TODO 迁移定时任务存储RUNNING点位规则 +// DevicePointRulesDO devicePoints = getDevicePointRules(deviceId); +// if (StringUtils.isBlank(devicePoints.getFieldRule())){ +// log.warn("设备 {} 没有RUNNING点位规则", device.getId()); +// +// DeviceOperationRecordDO record = new DeviceOperationRecordDO(); +// record.setDeviceId(deviceId); +// record.setRule(DeviceStatusEnum.STANDBY.getCode()); +// //TODO 待优化 +// record.setTotalStandbyTime(device.getSampleCycle()); +// record.setCreator("1"); +// record.setUpdater("1"); +// +// deviceOperationRecordMapper.insert(record); +// +// } @@ -484,21 +483,22 @@ public class MqttDataHandler extends SuperConsumer { //分别处理运行记录和告警记录 if (StringUtils.isBlank(devicePointRulesDO.getAlarmLevel())) { - DeviceOperationRecordDO record = new DeviceOperationRecordDO(); - record.setDeviceId(device.getId()); - record.setModelId(modelId); - record.setRule(pointRulesRespVO.getRule()); - record.setAddressValue(processedValue); - record.setRecordType(getRecordType(devicePointRulesDO)); - record.setRuleId(devicePointRulesDO.getId()); - //TODO 创建人和更新人为内置默认管理员 - record.setCreator("1"); - record.setUpdater("1"); - - // 处理累计时间 - calculateAndSetTotalTime(record, pointRulesRespVO.getRule(), device.getSampleCycle()); - - deviceOperationRecordMapper.insert(record); + //TODO 迁移运行记录到定时任务 +// DeviceOperationRecordDO record = new DeviceOperationRecordDO(); +// record.setDeviceId(device.getId()); +// record.setModelId(modelId); +// record.setRule(pointRulesRespVO.getRule()); +// record.setAddressValue(processedValue); +// record.setRecordType(getRecordType(devicePointRulesDO)); +// record.setRuleId(devicePointRulesDO.getId()); +// //TODO 创建人和更新人为内置默认管理员 +// record.setCreator("1"); +// record.setUpdater("1"); +// +// // 处理累计时间 +// calculateAndSetTotalTime(record, pointRulesRespVO.getRule(), device.getSampleCycle()); +// +// deviceOperationRecordMapper.insert(record); } else { DeviceWarinningRecordDO deviceWarinningRecordDO = new 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 c549cded1..f67dab4be 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 @@ -1,17 +1,35 @@ 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.alibaba.fastjson.JSONObject; +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.util.Date; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -28,35 +46,354 @@ public class DeviceJob implements JobHandler { @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().in(DeviceDO::getId, Arrays.asList(142L, 140L) ).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())) 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); + 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); + + } + + /** + * 判断值是否符合规则 + * 支持操作符: 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; + } - // 解析JSON字符串获取deviceId - JSONObject jsonParam = JSON.parseObject(param); - System.out.println(jsonParam + new Date().toString()); -// Long deviceId = jsonParam.getLong("deviceId"); -// log.info("定时任务执行,接收到的参数 param: {}", param); -// if (deviceId == null){ -// throw exception(DEVICE_DOES_NOT_EXIST); -// } - -// // 设置租户上下文 -// TenantContextHolder.setTenantId(1L); -// -// LambdaQueryWrapper deviceModelAttributeLambdaQueryWrapper = new LambdaQueryWrapper<>(); -// deviceModelAttributeLambdaQueryWrapper.eq(DeviceContactModelDO::getDeviceId,deviceId); -// List deviceContactModelDOS = deviceContactModelMapper.selectList(deviceModelAttributeLambdaQueryWrapper); -// -// if (deviceContactModelDOS != null && deviceContactModelDOS.size() > 0){ -// for (DeviceContactModelDO deviceContactModelDO : deviceContactModelDOS) { -// Object addressValue = OpcUtils.readValue(deviceContactModelDO.getAddress() != null ? deviceContactModelDO.getAddress() : ""); -// deviceContactModelDO.setAddressValue(addressValue); -// } -// -// } -// String json = JSON.toJSONString(deviceContactModelDOS); -// tDengineService.insertDeviceData(deviceId,json); - - return ""; + // 尝试转换为布尔值 + 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/DeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java index 98b11669c..cb8d7e3d0 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 @@ -37,15 +37,11 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceAttributeMapper; import cn.iocoder.yudao.module.iot.framework.mqtt.consumer.IMqttservice; -import cn.iocoder.yudao.module.iot.service.gateway.GatewayService; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -58,7 +54,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; -import javax.validation.constraints.NotNull; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.LocalDateTime; @@ -728,6 +723,25 @@ public class DeviceServiceImpl implements DeviceService { public PageResult lineDevicePage(LineDeviceRequestVO pageReqVO) { // 1. 查询分页设备 + // 如果有产线过滤条件 + if (StringUtils.isNotBlank(pageReqVO.getLineNode()) + || StringUtils.isNotBlank(pageReqVO.getLineName())) { + + List filteredDeviceIds = + deviceMapper.selectDeviceIdsByLine( + pageReqVO.getLineNode(), + pageReqVO.getLineName() + ); + + if (filteredDeviceIds.isEmpty()) { + return PageResult.empty(); + } + + // 把过滤后的 deviceIds 传给分页条件 + pageReqVO.setIds( filteredDeviceIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(","))); + } PageResult pageResult = getDevicePage(BeanUtils.toBean(pageReqVO, DevicePageReqVO.class)); @@ -748,6 +762,17 @@ public class DeviceServiceImpl implements DeviceService { .filter(Objects::nonNull) .collect(Collectors.toList()); + List lineList = + deviceMapper.selectLineBatch(deviceIds); + + Map lineMap = + lineList.stream() + .collect(Collectors.toMap( + LineCodeAndNameRespVO::getDeviceId, + Function.identity(), + (a, b) -> a + )); + // // 批量查 workshop // List> mapList = deviceMapper.selectWorkshopBatch(deviceIds); // @@ -795,9 +820,12 @@ public class DeviceServiceImpl implements DeviceService { } // 查询产线名称 - vo.setLineName( - deviceMapper.lineDeviceLedgerPage(device.getId()) - ); + LineCodeAndNameRespVO line = lineMap.get(device.getId()); + + if (line != null) { + vo.setLineName(line.getLineName()); + vo.setLineNode(line.getLineCode()); + } // vo.setLineName(workshopMap.get(device.getId())); @@ -1535,7 +1563,7 @@ public class DeviceServiceImpl implements DeviceService { log.info("gateway订阅记录已禁用 topic={}", topic); //更新设备运行状态为离线 - updateOperationalStatus(deviceDO); +// updateOperationalStatus(deviceDO); log.info("更新设备运行状态为离线 deviceId={}", deviceDO.getId()); 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 893b34431..c1a629f4a 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 @@ -25,6 +25,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -1193,7 +1194,13 @@ public class TDengineService { } - + /** + * 查每个设备在时间范围内的最新一条整行数据 + * @param deviceIds + * @param startTime + * @param endTime + * @return + */ @DS("tdengine") public Map> queryDevicesLatestRow( List deviceIds, @@ -1251,6 +1258,49 @@ public class TDengineService { return result; } + + /** + * 只返回每个设备最新一条数据的 ts 时间 + * @param deviceIds + * @return + */ + @DS("tdengine") + public Map queryDevicesLatestTs(List deviceIds) { + + if (CollectionUtils.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + + for (Long deviceId : deviceIds) { + + String sql = "SELECT LAST(ts) AS ts FROM besure_server.d_" + deviceId; + + try { + + List> list = + jdbcTemplate.queryForList(sql); + + if (!list.isEmpty() && list.get(0).get("ts") != null) { + + Timestamp ts = (Timestamp) list.get(0).get("ts"); + + result.put(deviceId, ts.toInstant()); + } + + } catch (Exception e) { + log.error("查询设备最新时间失败 deviceId={}", deviceId, e); + } + } + + return result; + } + + + + + @DS("tdengine") public Map> queryDevicesEarliestRow( List deviceIds, 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 4c0b1ed69..a23cba46f 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 @@ -10,6 +10,9 @@ import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO; @@ -92,49 +95,80 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe } - private void calculateAndSetConvertedValues(List records,DeviceTotalTimeRecordReqVO deviceTotalTimeRecordReqVO) { + private void calculateAndSetConvertedValues( + List records, + DeviceTotalTimeRecordReqVO reqVO) { + for (DeviceTotalTimeRecordRespVO record : records) { + try { - //添加时间字段 - if (StringUtils.isNotBlank(deviceTotalTimeRecordReqVO.getStartTime())){ - record.setStartTime(deviceTotalTimeRecordReqVO.getStartTime()); + + // 1设置查询时间 + String startTimeStr = reqVO.getStartTime(); + String endTimeStr = reqVO.getEndTime(); + + if (StringUtils.isNotBlank(startTimeStr)) { + record.setStartTime(startTimeStr); } - if (StringUtils.isNotBlank(deviceTotalTimeRecordReqVO.getEndTime())){ - record.setEndTime(deviceTotalTimeRecordReqVO.getEndTime()); + if (StringUtils.isNotBlank(endTimeStr)) { + record.setEndTime(endTimeStr); } // 获取原始秒数 - double runningTimeSec = record.getTotalRunningTime(); - double standbyTimeSec = record.getTotalStandbyTime(); - double faultTimeSec = record.getTotalFaultTime(); - double warningTimeSec = record.getTotalWarningTime(); - - // 1. 转换为小时 - double runningHours = TimeConverterUtil.secondsToHours(runningTimeSec, 2); - double standbyHours = TimeConverterUtil.secondsToHours(standbyTimeSec, 2); - double faultHours = TimeConverterUtil.secondsToHours(faultTimeSec, 2); - double warningHours = TimeConverterUtil.secondsToHours(warningTimeSec, 2); - - // 2. 计算稼动率 - double utilizationRate = TimeConverterUtil.calculateUtilizationRate( - runningTimeSec, standbyTimeSec, faultTimeSec, warningTimeSec - ); - - // 3. 设置转换后的值(将原来的秒数值覆盖为小时值) - record.setTotalRunningTime(runningHours); - record.setTotalStandbyTime(standbyHours); - record.setTotalFaultTime(faultHours); - record.setTotalWarningTime(warningHours); + double offlineSec = record.getTotalOfflineTime(); + double runningSec = record.getTotalRunningTime(); + double standbySec = record.getTotalStandbyTime(); + double faultSec = record.getTotalFaultTime(); + + // 在线时间 = 运行 + 待机 + 故障 + double onlineSec = runningSec + standbySec + faultSec; + + // 计算总时间(根据筛选时间) + double totalSec = 0; + if (StringUtils.isNotBlank(startTimeStr) && StringUtils.isNotBlank(endTimeStr)) { + + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + LocalDateTime start = LocalDateTime.parse(startTimeStr, formatter); + LocalDateTime end = LocalDateTime.parse(endTimeStr, formatter); + + totalSec = Duration.between(start, end).getSeconds(); + } + + // 防止负数或异常 + if (totalSec < 0) { + totalSec = 0; + } + + // 计算开机率 + double powerOnRate = 0; + if (totalSec > 0) { + powerOnRate = onlineSec / totalSec; + } + + // 计算稼动率 + double utilizationRate = 0; + if (onlineSec > 0) { + utilizationRate = runningSec / onlineSec; + } + + // 秒转小时(保留2位) + record.setTotalOfflineTime(TimeConverterUtil.secondsToHours(offlineSec, 2)); + record.setTotalRunningTime(TimeConverterUtil.secondsToHours(runningSec, 2)); + record.setTotalStandbyTime(TimeConverterUtil.secondsToHours(standbySec, 2)); + record.setTotalFaultTime(TimeConverterUtil.secondsToHours(faultSec, 2)); + + // 百分比字符串 + record.setPowerOnRate(TimeConverterUtil.getPercentString(powerOnRate)); record.setUtilizationRate(TimeConverterUtil.getPercentString(utilizationRate)); } catch (Exception e) { - log.error("计算设备{}时间转换时出错: {}", record.getDeviceCode(), e.getMessage()); - // 设置默认值 + log.error("计算设备{}时间统计出错: {}", record.getDeviceCode(), e.getMessage()); setDefaultValues(record); } } } - private void setDefaultValues(DeviceTotalTimeRecordRespVO record) { record.setTotalRunningTime(0.0); record.setTotalStandbyTime(0.0); 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 33ad11d49..c3e46238f 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 @@ -163,4 +163,34 @@ WHERE rn = 1 + + + \ 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 948fc71f5..8b6c36057 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 @@ -12,15 +12,21 @@