From db2eb9412ddc4ff687eb4c9b862053f49f9aec7b Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 26 八月 2026 13:16:42 +0800
Subject: [PATCH] feat(device): 完善 BTS 只读协议与模拟器

---
 ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsXmlCodec.java |   96 +++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 83 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 058135b..d0be888 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
@@ -14,6 +14,7 @@
 import java.util.Map;
 import java.util.Set;
 import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -21,9 +22,11 @@
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
 import com.ruoyi.device.model.ChannelIdentity;
 import com.ruoyi.device.model.DetectedChannel;
 import com.ruoyi.device.model.DeviceWorkStatus;
+import com.ruoyi.device.model.HistoryAvailability;
 import com.ruoyi.device.model.HistoryPage;
 import com.ruoyi.device.model.HistorySample;
 import com.ruoyi.device.model.Measurement;
@@ -68,6 +71,24 @@
         return envelope(list.append("</list>").toString());
     }
 
+    /**
+     * 查询各通道当前 BTS 测试的历史数据上传进度。
+     */
+    public String inquireHistoryRequest(Set<ChannelIdentity> channels)
+    {
+        StringBuilder list = new StringBuilder("<cmd>inquiredf</cmd><list count=\"").append(channels.size()).append("\">");
+        for (ChannelIdentity channel : channels)
+        {
+            requireOwned(channel);
+            list.append("<chl devtype=\"").append(channel.devtype())
+                    .append("\" devid=\"").append(channel.devid())
+                    .append("\" subdevid=\"").append(channel.subdevid())
+                    .append("\" chlid=\"").append(channel.chlid())
+                    .append("\" testid=\"0\"/>");
+        }
+        return envelope(list.append("</list>").toString());
+    }
+
     public String downloadRequest(ChannelIdentity channel, String btsTestId, long startPosition, int limit)
     {
         requireOwned(channel);
@@ -92,7 +113,7 @@
             ChannelIdentity identity = new ChannelIdentity(adapterCode, integerAttribute(element, "devtype"),
                     integerAttribute(element, "devid"), integerAttribute(element, "subdevid"), chlid);
             channels.add(new DetectedChannel(identity, requiredAttribute(element, "ip"),
-                    Boolean.parseBoolean(element.getTextContent().trim())));
+                    booleanText(element.getTextContent(), "available")));
         }
         return List.copyOf(channels);
     }
@@ -106,7 +127,9 @@
         {
             Element element = (Element) nodes.item(i);
             String[] device = requiredAttribute(element, "dev").split("-");
-            if (device.length < 4) throw new BtsProtocolException("Invalid BTS dev identity: " + element.getAttribute("dev"));
+            if (device.length < 4)
+                throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                        "Invalid BTS dev identity: " + element.getAttribute("dev"));
             ChannelIdentity identity = new ChannelIdentity(adapterCode, integer(device[0], "devtype"), integer(device[1], "devid"),
                     integer(device[2], "subdevid"), integer(device[3], "chlid"));
             snapshots.put(identity, new RealtimeSnapshot(identity,
@@ -119,11 +142,38 @@
         return Map.copyOf(snapshots);
     }
 
-    public HistoryPage parseHistory(String xml, ChannelIdentity channel, long requestedStart)
+    /**
+     * 将 BTS 历史清单映射为厂商无关的通道历史可用性。
+     */
+    public List<HistoryAvailability> parseHistoryAvailability(String xml)
+    {
+        Document document = parse(xml, "inquiredf_resp");
+        NodeList nodes = document.getElementsByTagName("chl");
+        List<HistoryAvailability> availability = new ArrayList<>(nodes.getLength());
+        for (int i = 0; i < nodes.getLength(); i++)
+        {
+            Element element = (Element) nodes.item(i);
+            ChannelIdentity channel = new ChannelIdentity(adapterCode, integerAttribute(element, "devtype"),
+                    integerAttribute(element, "devid"), integerAttribute(element, "subdevid"),
+                    integerAttribute(element, "chlid"));
+            availability.add(new HistoryAvailability(channel, requiredAttribute(element, "testid"),
+                    longAttribute(element, "count"), booleanText(element.getTextContent(), "uploadComplete")));
+        }
+        return List.copyOf(availability);
+    }
+
+    public HistoryPage parseHistory(String xml, ChannelIdentity channel, String requestedTestId, long requestedStart)
     {
         Document document = parse(xml, "download_resp");
         Element download = firstElement(document, "download");
         String btsTestId = requiredAttribute(download, "testid");
+        if (integerAttribute(download, "devtype") != channel.devtype()
+                || integerAttribute(download, "devid") != channel.devid()
+                || integerAttribute(download, "subdevid") != channel.subdevid()
+                || integerAttribute(download, "chlid") != channel.chlid()
+                || !btsTestId.equals(requestedTestId))
+            throw new BtsProtocolException(BtsProtocolException.Reason.IDENTITY_MISMATCH,
+                    "BTS history response identity does not match the request");
         NodeList nodes = document.getElementsByTagName("data");
         List<HistorySample> samples = new ArrayList<>(nodes.getLength());
         long next = requestedStart;
@@ -153,10 +203,13 @@
             factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
             factory.setXIncludeAware(false);
             factory.setExpandEntityReferences(false);
-            Document document = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            builder.setErrorHandler(new DefaultHandler());
+            Document document = builder.parse(new InputSource(new StringReader(xml)));
             String command = firstElement(document, "cmd").getTextContent().trim();
             if (!expectedCommand.equals(command))
-                throw new BtsProtocolException("Expected BTS command " + expectedCommand + " but received " + command);
+                throw new BtsProtocolException(BtsProtocolException.Reason.UNEXPECTED_COMMAND,
+                        "Expected BTS command " + expectedCommand + " but received " + command);
             return document;
         }
         catch (BtsProtocolException ex)
@@ -165,7 +218,8 @@
         }
         catch (Exception ex)
         {
-            throw new BtsProtocolException("Invalid BTS XML response", ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.MALFORMED_XML,
+                    "Invalid BTS XML response", ex);
         }
     }
 
@@ -201,7 +255,8 @@
         }
         catch (DateTimeParseException ex)
         {
-            throw new BtsProtocolException("Invalid BTS device time: " + value, ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                    "Invalid BTS device time: " + value, ex);
         }
     }
 
@@ -223,14 +278,16 @@
     private static Element firstElement(Document document, String tag)
     {
         NodeList elements = document.getElementsByTagName(tag);
-        if (elements.getLength() == 0) throw new BtsProtocolException("Missing BTS element: " + tag);
+        if (elements.getLength() == 0)
+            throw new BtsProtocolException(BtsProtocolException.Reason.MISSING_FIELD, "Missing BTS element: " + tag);
         return (Element) elements.item(0);
     }
 
     private static String requiredAttribute(Element element, String name)
     {
         String value = element.getAttribute(name);
-        if (value == null || value.isBlank()) throw new BtsProtocolException("Missing BTS attribute: " + name);
+        if (value == null || value.isBlank())
+            throw new BtsProtocolException(BtsProtocolException.Reason.MISSING_FIELD, "Missing BTS attribute: " + name);
         return value.trim();
     }
 
@@ -253,7 +310,8 @@
         }
         catch (NumberFormatException ex)
         {
-            throw new BtsProtocolException("Invalid integer BTS attribute: " + name, ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                    "Invalid integer BTS attribute: " + name, ex);
         }
     }
 
@@ -273,7 +331,8 @@
         }
         catch (NumberFormatException ex)
         {
-            throw new BtsProtocolException("Invalid integer BTS attribute: " + name, ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                    "Invalid integer BTS attribute: " + name, ex);
         }
     }
 
@@ -285,7 +344,8 @@
         }
         catch (NumberFormatException ex)
         {
-            throw new BtsProtocolException("Invalid integer BTS attribute: " + name, ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                    "Invalid integer BTS attribute: " + name, ex);
         }
     }
 
@@ -303,7 +363,17 @@
         }
         catch (NumberFormatException ex)
         {
-            throw new BtsProtocolException("Invalid decimal BTS attribute: " + name, ex);
+            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                    "Invalid decimal BTS attribute: " + name, ex);
         }
     }
+
+    private static boolean booleanText(String value, String name)
+    {
+        String normalized = value == null ? "" : value.trim();
+        if ("true".equalsIgnoreCase(normalized)) return true;
+        if ("false".equalsIgnoreCase(normalized)) return false;
+        throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
+                "Invalid boolean BTS value: " + name);
+    }
 }

--
Gitblit v1.7.1