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; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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 @RestController public class BtsDevelopmentSimulatorController { private final BtsDevelopmentSimulator simulator; private final DeviceDiscoveryCandidateStore candidateStore; private final com.ruoyi.config.DevelopmentCollectorStatusSynchronizer statusSynchronizer; @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); } /** 返回临时控制页;匿名放行后仍由本机拦截器限制来源。 */ @GetMapping(value = "/dev/bts-simulator.html", produces = MediaType.TEXT_HTML_VALUE) public Resource page() { return new ClassPathResource("static/dev/bts-simulator.html"); } /** 返回模拟器、虚拟设备、通道和最近请求的当前快照。 */ @GetMapping("/dev/bts-simulator/state") public StateSnapshot state() { 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 { 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) { throw badRequest(exception); } } /** 修改指定虚拟设备的地址、通道数和在线状态。 */ @PutMapping("/dev/bts-simulator/devices/{deviceId}") public StateSnapshot updateDevice(@PathVariable int deviceId, @RequestBody DeviceStateCommand command) { 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) { throw badRequest(exception); } } /** 删除指定虚拟设备。 */ @DeleteMapping("/dev/bts-simulator/devices/{deviceId}") public StateSnapshot deleteDevice(@PathVariable int deviceId) { 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 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) { throw badRequest(exception); } } /** 修改指定虚拟设备通道的在线和工作状态。 */ @PutMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}") public StateSnapshot updateChannel(@PathVariable int deviceId, @PathVariable int channelId, @RequestBody ChannelStateCommand command) { try { simulator.updateChannel(deviceId, channelId, command.online(), command.workStatus()); return simulator.snapshot(); } catch (IllegalArgumentException exception) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, exception.getMessage(), exception); } } /** 返回检测柜通道播放、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 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 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 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); } private void synchronizeOnlineStatusIfConfigured() { if (statusSynchronizer != null) statusSynchronizer.synchronizeOnlineStatus(); } 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 missingSequences, Long duplicateSequence, Long conflictingSequence, boolean outOfOrder) { public CollectorSamplingCommand { missingSequences = missingSequences == null ? List.of() : List.copyOf(missingSequences); } } public record CollectorChannelCommand(List metrics) { } public record CollectorMetricCommand(String metricCode, BigDecimal value, String unit) { } public record PlaybackCommand(Integer speedMultiplier, Boolean playing) { } }