yeg
2026-08-26 f9e7ed27f6ae71f6b2d89c40b422284a7e5411d2
fix(device): 完善 BTS 状态与模拟器契约
1 文件已重命名
1个文件已添加
3个文件已修改
88 ■■■■■ 已修改文件
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java
@@ -104,7 +104,7 @@
    public List<DetectedChannel> parseDeviceInfo(String xml)
    {
        Document document = parse(xml, "getdevinfo_resp");
        NodeList nodes = document.getElementsByTagName("channel");
        NodeList nodes = firstElement(document, "middle").getElementsByTagName("channel");
        List<DetectedChannel> channels = new ArrayList<>(nodes.getLength());
        for (int i = 0; i < nodes.getLength(); i++)
        {
@@ -121,7 +121,7 @@
    public Map<ChannelIdentity, RealtimeSnapshot> parseSnapshots(String xml, Instant receivedAt)
    {
        Document document = parse(xml, "inquire_resp");
        NodeList nodes = document.getElementsByTagName("inquire");
        NodeList nodes = firstElement(document, "list").getElementsByTagName("inquire");
        Map<ChannelIdentity, RealtimeSnapshot> snapshots = new LinkedHashMap<>();
        for (int i = 0; i < nodes.getLength(); i++)
        {
@@ -148,7 +148,7 @@
    public List<HistoryAvailability> parseHistoryAvailability(String xml)
    {
        Document document = parse(xml, "inquiredf_resp");
        NodeList nodes = document.getElementsByTagName("chl");
        NodeList nodes = firstElement(document, "list").getElementsByTagName("chl");
        List<HistoryAvailability> availability = new ArrayList<>(nodes.getLength());
        for (int i = 0; i < nodes.getLength(); i++)
        {
@@ -174,7 +174,7 @@
                || !btsTestId.equals(requestedTestId))
            throw new BtsProtocolException(BtsProtocolException.Reason.IDENTITY_MISMATCH,
                    "BTS history response identity does not match the request");
        NodeList nodes = document.getElementsByTagName("data");
        NodeList nodes = firstElement(document, "list").getElementsByTagName("data");
        List<HistorySample> samples = new ArrayList<>(nodes.getLength());
        long next = requestedStart;
        for (int i = 0; i < nodes.getLength(); i++)
ruoyi-device/src/main/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
File was renamed from ruoyi-device/src/test/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
@@ -16,7 +16,10 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
final class ScriptedBtsTcpSimulator implements AutoCloseable
/**
 * 提供可复用的 BTS TCP 脚本设备边界,用于闭环与恢复测试。
 */
public final class ScriptedBtsTcpSimulator implements AutoCloseable
{
    private static final byte[] FRAME_END = "#\r\n".getBytes(StandardCharsets.UTF_8);
@@ -26,7 +29,7 @@
    private final BlockingQueue<ScriptedAction> actions = new LinkedBlockingQueue<>();
    private final List<String> requests = new CopyOnWriteArrayList<>();
    ScriptedBtsTcpSimulator() throws IOException
    public ScriptedBtsTcpSimulator() throws IOException
    {
        this(true);
    }
@@ -41,38 +44,38 @@
            serverSocket.close();
    }
    static ScriptedBtsTcpSimulator refusingConnections() throws IOException
    public static ScriptedBtsTcpSimulator refusingConnections() throws IOException
    {
        return new ScriptedBtsTcpSimulator(false);
    }
    int port()
    public int port()
    {
        return port;
    }
    void enqueue(String response)
    public void enqueue(String response)
    {
        actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false));
    }
    void enqueueDelayed(Duration delay, String response)
    public void enqueueDelayed(Duration delay, String response)
    {
        if (delay.isNegative()) throw new IllegalArgumentException("delay must not be negative");
        actions.add(new ScriptedAction(delay, response.getBytes(StandardCharsets.UTF_8), false));
    }
    void enqueueRaw(byte[] response)
    public void enqueueRaw(byte[] response)
    {
        actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false));
    }
    void enqueueDisconnect()
    public void enqueueDisconnect()
    {
        actions.add(new ScriptedAction(Duration.ZERO, null, true));
    }
    List<String> requests()
    public List<String> requests()
    {
        return List.copyOf(requests);
    }
ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java
@@ -5,6 +5,7 @@
public enum DeviceWorkStatus
{
    WORKING,
    FINISHED,
    STOPPED,
    PROTECTED,
    PAUSED,
@@ -21,6 +22,7 @@
        return switch (value.trim().toLowerCase(Locale.ROOT))
        {
            case "working" -> WORKING;
            case "finish" -> FINISHED;
            case "stop" -> STOPPED;
            case "protect" -> PROTECTED;
            case "pause" -> PAUSED;
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java
@@ -77,6 +77,17 @@
    }
    @Test
    void mapsFinishAsNormalCompletion()
    {
        String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\">"
                + "<inquire dev=\"22-1-1-1-0\" workstatus=\"finish\"/></list></bts>";
        RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next();
        assertEquals(DeviceWorkStatus.FINISHED, snapshot.workStatus());
    }
    @Test
    void parsesHistoryPageAndHongKongDeviceTime()
    {
        ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
@@ -164,6 +175,27 @@
    }
    @Test
    void rejectsResponsesMissingRequiredContainers()
    {
        BtsProtocolException discovery = assertThrows(BtsProtocolException.class,
                () -> codec.parseDeviceInfo("<bts><cmd>getdevinfo_resp</cmd></bts>"));
        BtsProtocolException realtime = assertThrows(BtsProtocolException.class,
                () -> codec.parseSnapshots("<bts><cmd>inquire_resp</cmd></bts>", Instant.EPOCH));
        BtsProtocolException availability = assertThrows(BtsProtocolException.class,
                () -> codec.parseHistoryAvailability("<bts><cmd>inquiredf_resp</cmd></bts>"));
        ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
        BtsProtocolException history = assertThrows(BtsProtocolException.class,
                () -> codec.parseHistory("<bts><cmd>download_resp</cmd>"
                                + "<download devtype=\"24\" devid=\"1\" subdevid=\"1\" chlid=\"1\" testid=\"42\"/>"
                                + "</bts>", channel, "42", 1));
        assertEquals(BtsProtocolException.Reason.MISSING_FIELD, discovery.reason());
        assertEquals(BtsProtocolException.Reason.MISSING_FIELD, realtime.reason());
        assertEquals(BtsProtocolException.Reason.MISSING_FIELD, availability.reason());
        assertEquals(BtsProtocolException.Reason.MISSING_FIELD, history.reason());
    }
    @Test
    void rejectsDoctypeAndExternalEntities()
    {
        String xml = "<!DOCTYPE bts [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><bts><cmd>inquire_resp</cmd>&xxe;</bts>";
ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
New file
@@ -0,0 +1,25 @@
package com.ruoyi.device.contract;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import com.ruoyi.device.bts.BtsTcpTransport;
import com.ruoyi.device.bts.ScriptedBtsTcpSimulator;
class SimulatorPublicContractTest
{
    @Test
    void simulatorIsReusableOutsideTheBtsPackage() throws Exception
    {
        try (ScriptedBtsTcpSimulator simulator = new ScriptedBtsTcpSimulator();
             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
                     Duration.ofSeconds(1), Duration.ofSeconds(1)))
        {
            simulator.enqueue("<bts><cmd>getdevinfo_resp</cmd><middle count=\"0\"/></bts>");
            assertEquals("<bts><cmd>getdevinfo_resp</cmd><middle count=\"0\"/></bts>",
                    transport.exchange("<bts><cmd>getdevinfo</cmd></bts>"));
            assertEquals(1, simulator.requests().size());
        }
    }
}