package com.ruoyi.wrzs.monitor.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.wrzs.monitor.mapper.MonitorAlarmMapper; import com.ruoyi.wrzs.monitor.mapper.MonitorControlMapper; import com.ruoyi.wrzs.monitor.mapper.MonitorDataMapper; import com.ruoyi.wrzs.monitor.mapper.MonitorDeviceMapper; import com.ruoyi.wrzs.monitor.mapper.MonitorMetricMapper; import com.ruoyi.wrzs.monitor.pojo.MonitorAlarm; import com.ruoyi.wrzs.monitor.pojo.MonitorControl; import com.ruoyi.wrzs.monitor.pojo.MonitorData; import com.ruoyi.wrzs.monitor.pojo.MonitorDevice; import com.ruoyi.wrzs.monitor.pojo.MonitorMetric; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.InetSocketAddress; import java.net.Socket; 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; /** * 远程监控设备业务 *

* 设备(温度/压力/能耗) -> 指标阈值监测 -> 越限自动报警 -> 远程启停控制 * 不支持在线监测/远程控制的设备如实标记,页面不做假数据,支持人工补录与人工登记 */ @Slf4j @AllArgsConstructor @Service public class MonitorService { private final MonitorDeviceMapper deviceMapper; private final MonitorMetricMapper metricMapper; private final MonitorDataMapper dataMapper; private final MonitorAlarmMapper alarmMapper; private final MonitorControlMapper controlMapper; // ==================== 设备台账 ==================== public IPage devicePage(Page page, MonitorDevice query) { return deviceMapper.selectPage(page, Wrappers.lambdaQuery() .like(StringUtils.hasText(query.getDeviceName()), MonitorDevice::getDeviceName, query.getDeviceName()) .like(StringUtils.hasText(query.getDeviceCode()), MonitorDevice::getDeviceCode, query.getDeviceCode()) .eq(StringUtils.hasText(query.getDeviceType()), MonitorDevice::getDeviceType, query.getDeviceType()) .eq(StringUtils.hasText(query.getOnlineStatus()), MonitorDevice::getOnlineStatus, query.getOnlineStatus()) .eq(StringUtils.hasText(query.getLocation()), MonitorDevice::getLocation, query.getLocation()) .orderByDesc(MonitorDevice::getId)); } public List deviceList() { return deviceMapper.selectList(Wrappers.lambdaQuery().orderByAsc(MonitorDevice::getDeviceCode)); } /** 设备详情(附带监测指标) */ public Map deviceInfo(Long id) { Map result = new HashMap<>(); MonitorDevice device = deviceMapper.selectById(id); result.put("device", device); result.put("metrics", metricList(id)); return result; } @Transactional(rollbackFor = Exception.class) public boolean deviceSave(MonitorDevice device) { if (!StringUtils.hasText(device.getOnlineStatus())) { device.setOnlineStatus("1"); } if (!StringUtils.hasText(device.getRunStatus())) { device.setRunStatus("0"); } if (!StringUtils.hasText(device.getMonitorEnabled())) { device.setMonitorEnabled("1"); } if (!StringUtils.hasText(device.getControlEnabled())) { device.setControlEnabled("0"); } return deviceMapper.insert(device) > 0; } public boolean deviceUpdate(MonitorDevice device) { return deviceMapper.updateById(device) > 0; } public boolean deviceRemove(List ids) { if (CollectionUtils.isEmpty(ids)) { return false; } metricMapper.delete(Wrappers.lambdaQuery().in(MonitorMetric::getDeviceId, ids)); return deviceMapper.deleteBatchIds(ids) > 0; } // ==================== 监测指标 ==================== public List metricList(Long deviceId) { return metricMapper.selectList(Wrappers.lambdaQuery() .eq(deviceId != null, MonitorMetric::getDeviceId, deviceId) .orderByAsc(MonitorMetric::getId)); } public boolean metricSave(MonitorMetric metric) { if (!StringUtils.hasText(metric.getAlarmEnabled())) { metric.setAlarmEnabled("1"); } if (!StringUtils.hasText(metric.getCollectType())) { metric.setCollectType("0"); } return metricMapper.insert(metric) > 0; } public boolean metricUpdate(MonitorMetric metric) { return metricMapper.updateById(metric) > 0; } public boolean metricRemove(List ids) { if (CollectionUtils.isEmpty(ids)) { return false; } return metricMapper.deleteBatchIds(ids) > 0; } // ==================== 数据采集与报警 ==================== /** 监测历史数据(用于实时监测趋势曲线) */ public List dataHistory(Long deviceId, String metricCode, String startTime, String endTime) { return dataMapper.selectList(Wrappers.lambdaQuery() .eq(deviceId != null, MonitorData::getDeviceId, deviceId) .eq(StringUtils.hasText(metricCode), MonitorData::getMetricCode, metricCode) .ge(StringUtils.hasText(startTime), MonitorData::getCollectTime, startTime) .le(StringUtils.hasText(endTime), MonitorData::getCollectTime, endTime) .orderByAsc(MonitorData::getCollectTime) .last("limit 500")); } /** * 采集数据上报(自动采集端调用,也可由页面模拟采集) * 写入历史数据、更新指标当前值,并做阈值报警判定 */ @Transactional(rollbackFor = Exception.class) public MonitorAlarm dataReport(Long deviceId, String metricCode, BigDecimal value, String dataSource) { MonitorDevice device = deviceMapper.selectById(deviceId); if (device == null) { throw new IllegalArgumentException("设备不存在"); } MonitorMetric metric = metricMapper.selectOne(Wrappers.lambdaQuery() .eq(MonitorMetric::getDeviceId, deviceId) .eq(MonitorMetric::getMetricCode, metricCode) .last("limit 1")); if (metric == null) { throw new IllegalArgumentException("未配置该监测指标"); } LocalDateTime now = LocalDateTime.now(); metric.setCurrentValue(value); metric.setLastCollectTime(now); metric.setCollectType("1".equals(dataSource) ? "1" : "0"); metricMapper.updateById(metric); MonitorData data = new MonitorData(); data.setDeviceId(deviceId); data.setDeviceName(device.getDeviceName()); data.setMetricCode(metric.getMetricCode()); data.setMetricName(metric.getMetricName()); data.setMetricValue(value); data.setUnit(metric.getUnit()); data.setCollectTime(now); data.setDataSource("1".equals(dataSource) ? "1" : "0"); dataMapper.insert(data); device.setLastHeartbeat(now); device.setOnlineStatus("0"); deviceMapper.updateById(device); return checkAlarm(device, metric, value, now); } /** 阈值判断,越限生成报警(同一指标未处理的报警不重复生成,只更新数值) */ private MonitorAlarm checkAlarm(MonitorDevice device, MonitorMetric metric, BigDecimal value, LocalDateTime now) { if (!"1".equals(metric.getAlarmEnabled()) || value == null) { return null; } boolean belowMin = metric.getThresholdMin() != null && value.compareTo(metric.getThresholdMin()) < 0; boolean aboveMax = metric.getThresholdMax() != null && value.compareTo(metric.getThresholdMax()) > 0; if (!belowMin && !aboveMax) { return null; } MonitorAlarm exists = alarmMapper.selectOne(Wrappers.lambdaQuery() .eq(MonitorAlarm::getDeviceId, device.getId()) .eq(MonitorAlarm::getMetricCode, metric.getMetricCode()) .eq(MonitorAlarm::getAlarmStatus, "0") .orderByDesc(MonitorAlarm::getId) .last("limit 1")); MonitorAlarm alarm = exists == null ? new MonitorAlarm() : exists; alarm.setDeviceId(device.getId()); alarm.setDeviceName(device.getDeviceName()); alarm.setLocation(device.getLocation()); alarm.setMetricCode(metric.getMetricCode()); alarm.setMetricName(metric.getMetricName()); alarm.setAlarmValue(value); alarm.setUnit(metric.getUnit()); alarm.setThresholdMin(metric.getThresholdMin()); alarm.setThresholdMax(metric.getThresholdMax()); alarm.setAlarmLevel(resolveLevel(metric, value)); alarm.setAlarmContent(buildAlarmContent(device, metric, value)); alarm.setAlarmTime(now); alarm.setAlarmStatus("0"); alarm.setPushStatus("1"); if (exists == null) { alarmMapper.insert(alarm); } else { alarmMapper.updateById(alarm); } // 设备状态置为报警 device.setOnlineStatus("2"); deviceMapper.updateById(device); return alarm; } private String resolveLevel(MonitorMetric metric, BigDecimal value) { BigDecimal base = null; if (metric.getThresholdMax() != null && value.compareTo(metric.getThresholdMax()) > 0) { base = metric.getThresholdMax(); } else if (metric.getThresholdMin() != null && value.compareTo(metric.getThresholdMin()) < 0) { base = metric.getThresholdMin(); } if (base == null || base.compareTo(BigDecimal.ZERO) == 0) { return "1"; } BigDecimal rate = value.subtract(base).abs().multiply(BigDecimal.valueOf(100)) .divide(base.abs(), 2, RoundingMode.HALF_UP); if (rate.compareTo(BigDecimal.valueOf(15)) >= 0) { return "2"; } if (rate.compareTo(BigDecimal.valueOf(5)) >= 0) { return "1"; } return "0"; } private String buildAlarmContent(MonitorDevice device, MonitorMetric metric, BigDecimal value) { String threshold = "下限 " + (metric.getThresholdMin() == null ? "-" : metric.getThresholdMin()) + " / 上限 " + (metric.getThresholdMax() == null ? "-" : metric.getThresholdMax()); return device.getDeviceName() + " " + metric.getMetricName() + " 当前值 " + value + (StringUtils.hasText(metric.getUnit()) ? metric.getUnit() : "") + " 超出阈值(" + threshold + "),请及时处理"; } // ==================== 报警管理 ==================== public IPage alarmPage(Page page, MonitorAlarm query) { return alarmMapper.selectPage(page, Wrappers.lambdaQuery() .like(StringUtils.hasText(query.getDeviceName()), MonitorAlarm::getDeviceName, query.getDeviceName()) .eq(StringUtils.hasText(query.getAlarmStatus()), MonitorAlarm::getAlarmStatus, query.getAlarmStatus()) .eq(StringUtils.hasText(query.getAlarmLevel()), MonitorAlarm::getAlarmLevel, query.getAlarmLevel()) .orderByDesc(MonitorAlarm::getId)); } public long unhandledAlarmCount() { return alarmMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorAlarm::getAlarmStatus, "0")); } public List unhandledAlarms() { return alarmMapper.selectList(Wrappers.lambdaQuery() .eq(MonitorAlarm::getAlarmStatus, "0") .orderByDesc(MonitorAlarm::getAlarmTime) .last("limit 20")); } /** 处理报警 */ public boolean alarmHandle(Long id, String handleRemark) { MonitorAlarm alarm = alarmMapper.selectById(id); if (alarm == null) { return false; } alarm.setAlarmStatus("1"); alarm.setHandleTime(LocalDateTime.now()); alarm.setHandleRemark(handleRemark); alarm.setHandleUser(currentUserName()); boolean updated = alarmMapper.updateById(alarm) > 0; // 该设备已无未处理报警则恢复在线状态 Long left = alarmMapper.selectCount(Wrappers.lambdaQuery() .eq(MonitorAlarm::getDeviceId, alarm.getDeviceId()) .eq(MonitorAlarm::getAlarmStatus, "0")); if (left == null || left == 0) { MonitorDevice device = deviceMapper.selectById(alarm.getDeviceId()); if (device != null) { device.setOnlineStatus("0"); deviceMapper.updateById(device); } } return updated; } public boolean alarmRemove(List ids) { if (CollectionUtils.isEmpty(ids)) { return false; } return alarmMapper.deleteBatchIds(ids) > 0; } // ==================== 远程控制 ==================== /** * 远程启停控制 *

* 不支持远程控制的设备直接拒绝,避免页面上出现"假按钮"。 *

* 执行流程(每一步都写进 exec_detail,前端可展开查看通讯日志): * 1. 校验设备与远程控制开关 * 2. 若配置了合法 IP,先做真实 TCP 连通性探测(Modbus 502 端口,800ms 超时) * 3. 当前未接入现场设备,进入模拟模式:不写控制寄存器,但如实记录探测结果 * 4. 更新设备状态并记录耗时 */ @Transactional(rollbackFor = Exception.class) public MonitorControl control(Long deviceId, String command, String remark) { MonitorDevice device = deviceMapper.selectById(deviceId); if (device == null) { throw new IllegalArgumentException("设备不存在"); } if (!"1".equals(device.getControlEnabled())) { throw new IllegalArgumentException("该设备未接入远程控制回路,不支持远程启停,请现场操作"); } long startMs = System.currentTimeMillis(); List logs = new ArrayList<>(); String protocol = StringUtils.hasText(device.getCommProtocol()) ? device.getCommProtocol() : "未配置"; String ip = StringUtils.hasText(device.getIpAddress()) ? device.getIpAddress().trim() : ""; logs.add(ts() + " 设备:" + device.getDeviceName() + ",协议:" + protocol + ",地址:" + (ip.isEmpty() ? "未配置" : ip)); String simulateTip; boolean ipValid = ip.matches("^\\d{1,3}(\\.\\d{1,3}){3}$"); if (ipValid) { logs.add(ts() + " 尝试连接 " + ip + ":502 …"); boolean tcpOk = probeTcp(ip, 502, 800); if (tcpOk) { logs.add(ts() + " TCP 连接成功"); logs.add(ts() + " 模拟模式:连通性正常,但未配置寄存器映射,未写入控制寄存器"); simulateTip = "设备在线(" + ip + ":502 可达),模拟模式指令未实际写入"; } else { logs.add(ts() + " 连接失败:" + ip + ":502 无响应(800ms 超时)"); logs.add(ts() + " 模拟模式:跳过控制寄存器写入"); simulateTip = "设备 " + ip + ":502 无法连接,指令未实际下发"; } } else { logs.add(ts() + " 设备未配置有效通讯地址,跳过连通性探测"); logs.add(ts() + " 模拟模式:跳过控制寄存器写入"); simulateTip = "设备未配置通讯地址,指令未实际下发"; } String execMsg; if ("stop".equalsIgnoreCase(command)) { device.setRunStatus("0"); execMsg = "停机指令已受理"; } else if ("start".equalsIgnoreCase(command)) { device.setRunStatus("1"); execMsg = "启动指令已受理"; } else if ("restart".equalsIgnoreCase(command)) { device.setRunStatus("1"); execMsg = "重启指令已受理"; } else { throw new IllegalArgumentException("不支持的控制指令"); } long duration = System.currentTimeMillis() - startMs; logs.add(ts() + " " + execMsg + ",耗时 " + duration + "ms"); MonitorControl control = new MonitorControl(); control.setDeviceId(deviceId); control.setDeviceName(device.getDeviceName()); control.setControlCommand(command); control.setControlType(StringUtils.hasText(device.getControlType()) ? device.getControlType() : "启停"); control.setCommandSource("远程控制"); control.setOperator(currentUserName()); control.setOperateTime(LocalDateTime.now()); control.setRemark(remark); control.setExecResult("0"); control.setExecDuration((int) duration); control.setResultMsg(execMsg + "(" + simulateTip + ")"); control.setExecDetail(String.join("\n", logs)); controlMapper.insert(control); deviceMapper.updateById(device); return control; } /** 当前时间戳 HH:mm:ss.SSS,用于通讯日志 */ private static String ts() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")); } /** TCP 连通性探测(真实连接,用于判断设备是否可达) */ private static boolean probeTcp(String host, int port, int timeoutMs) { Socket socket = new Socket(); try { socket.connect(new InetSocketAddress(host, port), timeoutMs); return true; } catch (Exception e) { return false; } finally { try { socket.close(); } catch (Exception ignored) { } } } public IPage controlPage(Page page, MonitorControl query) { return controlMapper.selectPage(page, Wrappers.lambdaQuery() .like(StringUtils.hasText(query.getDeviceName()), MonitorControl::getDeviceName, query.getDeviceName()) .eq(StringUtils.hasText(query.getControlCommand()), MonitorControl::getControlCommand, query.getControlCommand()) .orderByDesc(MonitorControl::getId)); } // ==================== 总览 ==================== public Map overview() { Map result = new HashMap<>(); long total = deviceMapper.selectCount(Wrappers.lambdaQuery()); long online = deviceMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorDevice::getOnlineStatus, "0")); long offline = deviceMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorDevice::getOnlineStatus, "1")); long alarming = deviceMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorDevice::getOnlineStatus, "2")); long monitorEnabled = deviceMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorDevice::getMonitorEnabled, "1")); long controlEnabled = deviceMapper.selectCount(Wrappers.lambdaQuery().eq(MonitorDevice::getControlEnabled, "1")); long unhandled = unhandledAlarmCount(); result.put("total", total); result.put("online", online); result.put("offline", offline); result.put("alarming", alarming); result.put("monitorEnabled", monitorEnabled); result.put("controlEnabled", controlEnabled); result.put("unhandledAlarm", unhandled); return result; } private String currentUserName() { try { return SecurityUtils.getLoginUser().getUser().getNickName(); } catch (Exception e) { return "系统"; } } }