yeg
2026-09-02 4eaa4be4d2cbd2aa49d36a313ce71ad3e743db4a
ruoyi-admin/src/main/java/com/ruoyi/web/controller/device/BtsDevelopmentSimulatorController.java
@@ -1,5 +1,7 @@
package com.ruoyi.web.controller.device;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
@@ -10,12 +12,20 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.beans.factory.annotation.Autowired;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.bts.BtsDevelopmentSimulator.StateSnapshot;
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore;
import com.ruoyi.device.model.DeviceWorkStatus;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorHistoryPage;
import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
import com.ruoyi.device.bts.BtsDevelopmentSimulator.PlaybackSnapshot;
/** 本机临时 BTS 模拟器控制 API。 */
@Anonymous
@@ -23,10 +33,24 @@
public class BtsDevelopmentSimulatorController
{
    private final BtsDevelopmentSimulator simulator;
    private final DeviceDiscoveryCandidateStore candidateStore;
    private final com.ruoyi.config.DevelopmentCollectorStatusSynchronizer statusSynchronizer;
    public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator)
    @Autowired
    public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator,
            DeviceDiscoveryCandidateStore candidateStore,
            com.ruoyi.config.DevelopmentCollectorStatusSynchronizer statusSynchronizer)
    {
        this.simulator = simulator;
        this.candidateStore = candidateStore;
        this.statusSynchronizer = statusSynchronizer;
    }
    /** 保留独立模拟器测试和工具调用的构造入口,不触发数据库同步。 */
    public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator,
            DeviceDiscoveryCandidateStore candidateStore)
    {
        this(simulator, candidateStore, null);
    }
    /** 返回临时控制页;匿名放行后仍由本机拦截器限制来源。 */
@@ -43,13 +67,36 @@
        return simulator.snapshot();
    }
    /** 人工清空长期状态并恢复一台默认检测柜和采集器。 */
    @PostMapping("/dev/bts-simulator/reset")
    public StateSnapshot reset()
    {
        simulator.reset();
        candidateStore.removeAdapter(BtsDevelopmentSimulator.ADAPTER_CODE);
        return simulator.snapshot();
    }
    /** 模拟现场人工统一开关全部检测柜和采集器,不发送任何设备控制命令。 */
    @PostMapping("/dev/bts-simulator/power")
    public StateSnapshot setPower(@RequestBody SimulatorPowerCommand command)
    {
        if (command == null || command.poweredOn() == null)
            throw badRequest(new IllegalArgumentException("poweredOn must be true or false"));
        simulator.setAllDevicesPoweredOn(command.poweredOn());
        synchronizeOnlineStatusIfConfigured();
        return simulator.snapshot();
    }
    /** 添加一台虚拟设备。 */
    @PostMapping("/dev/bts-simulator/devices")
    public StateSnapshot createDevice(@RequestBody CreateDeviceCommand command)
    {
        try
        {
            simulator.addDevice(command.ip(), command.channelCount());
            if (command.model() == null || command.model().isBlank())
                simulator.addDevice(command.ip(), command.channelCount());
            else
                simulator.addNewareDevice(command.ip(), command.model());
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
@@ -65,6 +112,8 @@
        try
        {
            simulator.updateDevice(deviceId, command.ip(), command.channelCount(), command.online());
            if (command.model() != null && !command.model().isBlank())
                simulator.configureNewareProfile(deviceId, command.model());
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
@@ -80,6 +129,119 @@
        try
        {
            simulator.deleteDevice(deviceId);
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 添加一台与检测柜分开配置的虚拟采集器。 */
    @PostMapping("/dev/bts-simulator/collectors")
    public StateSnapshot createCollector(@RequestBody CreateCollectorCommand command)
    {
        try
        {
            simulator.addCollector(command.ip(), command.channelCount());
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 修改虚拟采集器的地址、通道数和在线状态。 */
    @PutMapping("/dev/bts-simulator/collectors/{collectorId}")
    public StateSnapshot updateCollector(@PathVariable int collectorId,
            @RequestBody CollectorStateCommand command)
    {
        try
        {
            simulator.updateCollector(collectorId, command.ip(), command.channelCount(), command.online());
            synchronizeOnlineStatusIfConfigured();
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 配置采集器采样频率和可重复的历史下载故障场景。 */
    @PutMapping("/dev/bts-simulator/collectors/{collectorId}/sampling")
    public StateSnapshot configureCollectorSampling(@PathVariable int collectorId,
            @RequestBody CollectorSamplingCommand command)
    {
        try
        {
            simulator.configureCollectorSampling(collectorId, command.sampleIntervalMs(),
                    new CollectorHistoryFaults(command.missingSequences(), command.duplicateSequence(),
                            command.conflictingSequence(), command.outOfOrder()));
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 配置稳定 S 通道的固定和动态测量项。 */
    @PutMapping("/dev/bts-simulator/collectors/{collectorId}/channels/{channelCode}")
    public StateSnapshot configureCollectorChannel(@PathVariable int collectorId,
            @PathVariable String channelCode, @RequestBody CollectorChannelCommand command)
    {
        try
        {
            List<CollectorMetricState> metrics = command.metrics().stream()
                    .map(metric -> new CollectorMetricState(metric.metricCode(), metric.value(), metric.unit()))
                    .toList();
            simulator.configureCollectorChannel(collectorId, channelCode, metrics);
            return simulator.snapshot();
        }
        catch (IllegalArgumentException | NullPointerException exception)
        {
            throw badRequest(new IllegalArgumentException("Invalid collector channel metrics", exception));
        }
    }
    /** 查询采集器最新实时快照;离线时保留最后观测并明确返回离线事实。 */
    @GetMapping("/dev/bts-simulator/collectors/{collectorId}/realtime")
    public CollectorRealtimeSnapshot collectorRealtime(@PathVariable int collectorId)
    {
        try
        {
            return simulator.collectorRealtime(collectorId);
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 按偏移量分页下载采集器模拟历史,每页最多 1000 条。 */
    @GetMapping("/dev/bts-simulator/collectors/{collectorId}/history")
    public CollectorHistoryPage collectorHistory(@PathVariable int collectorId,
            @RequestParam(defaultValue = "0") long offset, @RequestParam(defaultValue = "100") int limit)
    {
        try
        {
            return simulator.collectorHistory(collectorId, offset, limit);
        }
        catch (IllegalArgumentException exception)
        {
            throw badRequest(exception);
        }
    }
    /** 删除指定虚拟采集器。 */
    @DeleteMapping("/dev/bts-simulator/collectors/{collectorId}")
    public StateSnapshot deleteCollector(@PathVariable int collectorId)
    {
        try
        {
            simulator.deleteCollector(collectorId);
            return simulator.snapshot();
        }
        catch (IllegalArgumentException exception)
@@ -104,14 +266,144 @@
        }
    }
    /** 返回检测柜通道播放、record、step 和 cycle 的当前快照。 */
    @GetMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback")
    public PlaybackSnapshot btsPlayback(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @GetMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/records")
    public List<com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord> btsRecords(
            @PathVariable int deviceId, @PathVariable int channelId)
    {
        try { return simulator.btsRecords(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @GetMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/steps")
    public List<BtsDevelopmentSimulator.BtsStepRecord> btsSteps(
            @PathVariable int deviceId, @PathVariable int channelId)
    {
        try { return simulator.btsSteps(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @GetMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/cycles")
    public List<BtsDevelopmentSimulator.BtsCycleRecord> btsCycles(
            @PathVariable int deviceId, @PathVariable int channelId)
    {
        try { return simulator.btsCycles(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    /** 设置检测柜通道演示倍速。 */
    @PutMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback")
    public PlaybackSnapshot configureBtsPlayback(@PathVariable int deviceId, @PathVariable int channelId,
            @RequestBody PlaybackCommand command)
    {
        try
        {
            if (command.speedMultiplier() != null) simulator.setBtsSpeed(deviceId, channelId, command.speedMultiplier());
            if (command.playing() != null)
            {
                if (command.playing()) simulator.playBts(deviceId, channelId);
                else simulator.pauseBts(deviceId, channelId);
            }
            return simulator.btsPlayback(deviceId, channelId);
        }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/play")
    public PlaybackSnapshot playBts(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.playBts(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/pause")
    public PlaybackSnapshot pauseBts(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.pauseBts(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/resume")
    public PlaybackSnapshot resumeBts(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.resumeBts(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/next-step")
    public PlaybackSnapshot nextBtsStep(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.nextBtsStep(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/next-cycle")
    public PlaybackSnapshot nextBtsCycle(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.nextBtsCycle(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    @PostMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/playback/reset")
    public PlaybackSnapshot resetBts(@PathVariable int deviceId, @PathVariable int channelId)
    {
        try { simulator.resetBts(deviceId, channelId); return simulator.btsPlayback(deviceId, channelId); }
        catch (IllegalArgumentException exception) { throw badRequest(exception); }
    }
    private ResponseStatusException badRequest(IllegalArgumentException exception)
    {
        return new ResponseStatusException(HttpStatus.BAD_REQUEST, exception.getMessage(), exception);
    }
    public record CreateDeviceCommand(String ip, int channelCount) { }
    private void synchronizeOnlineStatusIfConfigured()
    {
        if (statusSynchronizer != null) statusSynchronizer.synchronizeOnlineStatus();
    }
    public record DeviceStateCommand(String ip, int channelCount, boolean online) { }
    public record CreateDeviceCommand(String ip, int channelCount, String model)
    {
        public CreateDeviceCommand(String ip, int channelCount)
        {
            this(ip, channelCount, null);
        }
    }
    public record SimulatorPowerCommand(Boolean poweredOn) { }
    public record DeviceStateCommand(String ip, int channelCount, boolean online, String model)
    {
        public DeviceStateCommand(String ip, int channelCount, boolean online)
        {
            this(ip, channelCount, online, null);
        }
    }
    public record ChannelStateCommand(boolean online, DeviceWorkStatus workStatus) { }
    public record CreateCollectorCommand(String ip, int channelCount) { }
    public record CollectorStateCommand(String ip, int channelCount, boolean online) { }
    public record CollectorSamplingCommand(int sampleIntervalMs, List<Long> missingSequences,
            Long duplicateSequence, Long conflictingSequence, boolean outOfOrder)
    {
        public CollectorSamplingCommand
        {
            missingSequences = missingSequences == null ? List.of() : List.copyOf(missingSequences);
        }
    }
    public record CollectorChannelCommand(List<CollectorMetricCommand> metrics) { }
    public record CollectorMetricCommand(String metricCode, BigDecimal value, String unit) { }
    public record PlaybackCommand(Integer speedMultiplier, Boolean playing) { }
}