yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
@@ -23,17 +23,14 @@
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import com.battery.device.model.ChannelIdentity;
import com.battery.device.model.DeviceConnectionStatus;
import com.battery.device.model.DetectedChannel;
import com.battery.device.model.DetectedDeviceMetadata;
import com.battery.device.model.DeviceWorkStatus;
import com.battery.device.model.HistoryAvailability;
import com.battery.device.model.HistoryPage;
import com.battery.device.model.HistorySample;
import com.battery.device.model.Measurement;
import com.battery.device.model.PlannedStep;
import com.battery.device.model.RealtimeSnapshot;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DeviceConnectionStatus;
import com.battery.device.api.model.DetectedChannel;
import com.battery.device.api.model.DetectedDeviceMetadata;
import com.battery.device.api.model.DeviceWorkStatus;
import com.battery.device.api.model.Measurement;
import com.battery.device.api.model.PlannedStep;
import com.battery.device.api.model.RealtimeSnapshot;
public final class BtsXmlCodec
{
@@ -42,8 +39,6 @@
            "dev", "cycle_id", "step_id", "step_type", "workstatus", "barcode", "current", "voltage",
            "capacity", "energy", "totaltime", "relativetime", "auxtemp", "auxvol", "open_or_close", "log_code",
            "device_time", "deviceTime", "time", "atime", "testtime");
    private static final Set<String> HISTORY_ATTRIBUTES = Set.of(
            "seqid", "stepid", "cycleid", "steptype", "testtime", "atime", "volt", "curr", "cap", "eng");
    private final String adapterCode;
    private final ZoneId deviceZone;
@@ -55,11 +50,13 @@
        this.deviceZone = deviceZone;
    }
    /** 构造通道发现的请求报文({@code getdevinfo})。 */
    public String getDeviceInfoRequest()
    {
        return envelope("<cmd>getdevinfo</cmd>");
    }
    /** 构造按通道批量查询实时快照的请求报文({@code inquire});只接受属于本实例的通道身份。 */
    public String inquireRequest(Set<ChannelIdentity> channels, boolean includeBarcode)
    {
        StringBuilder list = new StringBuilder("<cmd>inquire</cmd><list count=\"").append(channels.size()).append("\">");
@@ -75,36 +72,9 @@
        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);
        if (btsTestId == null || btsTestId.isBlank()) throw new IllegalArgumentException("btsTestId must not be blank");
        if (startPosition < 1) throw new IllegalArgumentException("startPosition must be positive");
        if (limit < 1 || limit > 1000) throw new IllegalArgumentException("BTS history limit must be between 1 and 1000");
        return envelope("<cmd>download</cmd><download devtype=\"" + channel.devtype()
                + "\" devid=\"" + channel.devid() + "\" subdevid=\"" + channel.subdevid()
                + "\" chlid=\"" + channel.chlid() + "\" auxid=\"0\" testid=\"" + escape(btsTestId)
                + "\" startpos=\"" + startPosition + "\" count=\"" + limit + "\"/>");
    }
    /** 解析 {@code getdevinfo_resp},按本实例的适配器编码生成检测柜通道清单。 */
    public List<DetectedChannel> parseDeviceInfo(String xml)
    {
        Document document = parse(xml, "getdevinfo_resp");
@@ -125,6 +95,7 @@
        return List.copyOf(channels);
    }
    /** 解析 {@code inquire_resp} 并按通道身份归集实时快照;{@code receivedAt} 记为服务器接收时间。 */
    public Map<ChannelIdentity, RealtimeSnapshot> parseSnapshots(String xml, Instant receivedAt)
    {
        Document document = parse(xml, "inquire_resp");
@@ -151,56 +122,7 @@
        return Map.copyOf(snapshots);
    }
    /**
     * 将 BTS 历史清单映射为厂商无关的通道历史可用性。
     */
    public List<HistoryAvailability> parseHistoryAvailability(String xml)
    {
        Document document = parse(xml, "inquiredf_resp");
        NodeList nodes = firstElement(document, "list").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");
        // download 是单通道响应,身份不一致时拒绝结果,避免历史事实被归入错误测试。
        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 = firstElement(document, "list").getElementsByTagName("data");
        List<HistorySample> samples = new ArrayList<>(nodes.getLength());
        long next = requestedStart;
        for (int i = 0; i < nodes.getLength(); i++)
        {
            Element element = (Element) nodes.item(i);
            long sequence = longAttribute(element, "seqid");
            String rawTime = requiredAttribute(element, "atime");
            samples.add(new HistorySample(sequence, integerAttribute(element, "stepid"), integerAttribute(element, "cycleid"),
                    requiredAttribute(element, "steptype"), optionalLong(element, "testtime"), parseDeviceTime(rawTime), rawTime,
                    optionalDecimal(element, "volt"), optionalDecimal(element, "curr"), optionalDecimal(element, "cap"),
                    optionalDecimal(element, "eng"), dynamicMeasurements(element, HISTORY_ATTRIBUTES)));
            // 解析层保留重复和乱序事实;业务层只展示并记录这些设备数据事实。
            next = Math.max(next, sequence + 1);
        }
        return new HistoryPage(channel, btsTestId, requestedStart, next, samples);
    }
    private Document parse(String xml, String expectedCommand)
    {