From f9e7ed27f6ae71f6b2d89c40b422284a7e5411d2 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 26 八月 2026 13:29:53 +0800
Subject: [PATCH] fix(device): 完善 BTS 状态与模拟器契约

---
 ruoyi-device/src/main/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java          |   21 ++++++----
 ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java               |    2 +
 ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java                  |   32 ++++++++++++++++
 ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java                      |    8 ++--
 ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java |   25 ++++++++++++
 5 files changed, 75 insertions(+), 13 deletions(-)

diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java
index d0be888..8860b8f 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java
+++ b/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++)
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
similarity index 88%
rename from ruoyi-device/src/test/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
rename to ruoyi-device/src/main/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
index 5a61ea5..f4fb784 100644
--- a/ruoyi-device/src/test/java/com/ruoyi/device/bts/ScriptedBtsTcpSimulator.java
+++ b/ruoyi-device/src/main/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);
     }
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java b/ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java
index d7684eb..c302225 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/model/DeviceWorkStatus.java
+++ b/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;
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java b/ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java
index 2699acd..d6d2862 100644
--- a/ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsXmlCodecTest.java
+++ b/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>";
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java b/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
new file mode 100644
index 0000000..383fd6d
--- /dev/null
+++ b/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
@@ -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());
+        }
+    }
+}

--
Gitblit v1.7.1