src/main/java/com/ruoyi/wrzs/monitor/service/MonitorService.java
@@ -23,7 +23,11 @@
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;
@@ -129,6 +133,17 @@
    }
    // ==================== 数据采集与报警 ====================
    /** 监测历史数据(用于实时监测趋势曲线) */
    public List<MonitorData> dataHistory(Long deviceId, String metricCode, String startTime, String endTime) {
        return dataMapper.selectList(Wrappers.<MonitorData>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"));
    }
    /**
     * 采集数据上报(自动采集端调用,也可由页面模拟采集)
@@ -299,7 +314,14 @@
    /**
     * 远程启停控制
     * 不支持远程控制的设备直接拒绝,避免页面上出现"假按钮"
     * <p>
     * 不支持远程控制的设备直接拒绝,避免页面上出现"假按钮"。
     * <p>
     * 执行流程(每一步都写进 exec_detail,前端可展开查看通讯日志):
     * 1. 校验设备与远程控制开关
     * 2. 若配置了合法 IP,先做真实 TCP 连通性探测(Modbus 502 端口,800ms 超时)
     * 3. 当前未接入现场设备,进入模拟模式:不写控制寄存器,但如实记录探测结果
     * 4. 更新设备状态并记录耗时
     */
    @Transactional(rollbackFor = Exception.class)
    public MonitorControl control(Long deviceId, String command, String remark) {
@@ -310,7 +332,51 @@
        if (!"1".equals(device.getControlEnabled())) {
            throw new IllegalArgumentException("该设备未接入远程控制回路,不支持远程启停,请现场操作");
        }
        LocalDateTime now = LocalDateTime.now();
        long startMs = System.currentTimeMillis();
        List<String> 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());
@@ -318,26 +384,38 @@
        control.setControlType(StringUtils.hasText(device.getControlType()) ? device.getControlType() : "启停");
        control.setCommandSource("远程控制");
        control.setOperator(currentUserName());
        control.setOperateTime(now);
        control.setOperateTime(LocalDateTime.now());
        control.setRemark(remark);
        if ("stop".equalsIgnoreCase(command)) {
            device.setRunStatus("0");
            control.setResultMsg("停机指令已下发");
        } else if ("start".equalsIgnoreCase(command)) {
            device.setRunStatus("1");
            control.setResultMsg("启动指令已下发");
        } else if ("restart".equalsIgnoreCase(command)) {
            device.setRunStatus("1");
            control.setResultMsg("重启指令已下发");
        } else {
            throw new IllegalArgumentException("不支持的控制指令");
        }
        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<MonitorControl> controlPage(Page<MonitorControl> page, MonitorControl query) {
        return controlMapper.selectPage(page, Wrappers.<MonitorControl>lambdaQuery()
                .like(StringUtils.hasText(query.getDeviceName()), MonitorControl::getDeviceName, query.getDeviceName())