| | |
| | | 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.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 |
| | |
| | | { |
| | | private final BtsDevelopmentSimulator simulator; |
| | | private final DeviceDiscoveryCandidateStore candidateStore; |
| | | private final com.ruoyi.config.DevelopmentCollectorStatusSynchronizer statusSynchronizer; |
| | | |
| | | @Autowired |
| | | public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator, |
| | | DeviceDiscoveryCandidateStore candidateStore) |
| | | 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); |
| | | } |
| | | |
| | | /** 返回临时控制页;匿名放行后仍由本机拦截器限制来源。 */ |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | 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) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | /** 返回检测柜通道播放、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) { } |
| | | } |