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 "系统";
}
}
}