From e38aca8134570513435c003c25dc05f1ae51c9c9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 08 十月 2026 13:56:28 +0800
Subject: [PATCH] 实现 Ticket04:采集器辅助测量与登记入口,拆除开发模拟器

---
 battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java          |   78 ++
 battery-business/src/main/java/com/battery/business/mapper/TCollectorMapper.java                             |    7 
 battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java                   |   84 +-
 pom.xml                                                                                                      |   14 
 scripts/clear-simulator-development-data.sql                                                                 |  114 +++
 battery-business/src/test/java/com/battery/business/service/impl/PushIngestReceptionContractTest.java        |   73 ++
 ruoyi-admin/src/main/java/com/battery/business/web/TCollectorController.java                                 |   34 -
 ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java                       |    3 
 ruoyi-admin/src/test/java/com/battery/business/runtime/DefaultSpringProfileContractTest.java                 |    6 
 battery-device-neware/src/test/java/com/battery/device/neware/ReadOnlyVendorDataSourceTest.java              |   33 +
 ruoyi-admin/src/test/java/com/battery/business/service/impl/TTestLiveServiceImplTest.java                    |  213 ++++--
 ruoyi-admin/src/test/java/com/battery/business/runtime/VendorSourceInstanceMigrationContractTest.java        |    4 
 ruoyi-admin/pom.xml                                                                                          |   33 -
 ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java                   |  172 ++++-
 ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeNoopStartupTest.java              |    1 
 battery-device-api/src/main/java/com/battery/device/api/discovery/DeviceDiscoveryCandidateStore.java         |   16 
 battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java |   15 
 battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java                  |   29 
 battery-business/src/main/java/com/battery/business/service/ITCollectorService.java                          |    4 
 ruoyi-admin/src/main/resources/application.yml                                                               |    2 
 battery-business/src/main/java/com/battery/business/domain/TCollector.java                                   |    6 
 ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java               |   48 -
 /dev/null                                                                                                    |  348 ------------
 battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml                                      |   33 
 battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java                  |  118 ++--
 ruoyi-admin/src/test/java/com/battery/business/service/impl/RoutingAdapterWiringContractTest.java            |   11 
 battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java                        |    7 
 AGENTS.md                                                                                                    |    4 
 ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceServiceTest.java       |   63 +
 battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java                      |  113 +++
 ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java                       |    3 
 31 files changed, 870 insertions(+), 819 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index fa5a7fe..76e5961 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,7 +5,7 @@
 ## 模块边界
 
 - `battery-business` 负责测试业务、状态聚合、事务、持久化、修订和异常;它只依赖 `battery-device-api`,不解析 BTS XML 或依赖 Redis/模拟器。
-- `battery-device-api` 提供厂商无关观测模型;`battery-device-bts` 封装协议;`battery-device-simulator` 提供开发实现。协议 XML、TCP 帧和厂商枚举停留在 BTS 模块内部。
+- `battery-device-api` 提供厂商无关观测模型;`battery-device-bts` 封装协议;直连厂商存储的实现放在 `battery-device-neware`,读本项目落地表的推式实现放在 `battery-business`(开发模拟器模块已按 Ticket04 整体拆除)。协议 XML、TCP 帧和厂商枚举停留在 BTS 模块内部。
 - `ruoyi-admin` 只负责应用装配及 REST/SSE 控制器,不承载采集或状态机业务。
 - 第一阶段打包为一个后端进程,并明确保持单采集实例;最终业务基线不建立适配器租约模型。
 
@@ -45,4 +45,4 @@
 - 设备协议或调度改动至少覆盖正常多通道、拒绝连接、断线、超时、畸形 XML 和实时数据幂等冲突等受影响场景。
 - 运行受影响 `battery-*` 模块测试;跨模块契约变更运行全量测试 `mvn test`。
 - 验证可执行包:`mvn -pl ruoyi-admin -am package -DskipTests`。启动验证时使用可写的 `LOG_PATH`,完成后停止临时进程。
-- 浏览器主接缝由前端仓库 `npm run test:e2e:ticket04` 编排隔离 MySQL 8、Redis、完整可执行包和模拟器;不得连接共享开发库或复用已有服务端口。
+- 浏览器主接缝由前端仓库 `npm run test:e2e:ticket04` 编排隔离 MySQL 8、Redis、完整可执行包;不得连接共享开发库或复用已有服务端口。**该编排原本还会启动开发模拟器,模拟器已按 Ticket04 整体拆除**,`tests/e2e/` 下依赖模拟器页面与端点的用例在按真实来源重写之前跑不通(见 `docs/KNOWN-GAPS.md`)。
diff --git a/battery-business/src/main/java/com/battery/business/domain/TCollector.java b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
index 4196760..a784699 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TCollector.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
@@ -44,13 +44,11 @@
     @Size(max = 64, message = "设备编码长度不能超过64个字符")
     private String collectorCode;
 
-    /** IP 地址 */
-    @NotBlank(message = "IP地址不能为空")
+    /** IP 地址;辅助测量不是独立硬件,可为空,填了必须合法。 */
     @Size(max = 45, message = "IP地址长度不能超过45个字符")
     private String ipAddress;
 
-    /** MAC 地址 */
-    @NotBlank(message = "MAC地址不能为空")
+    /** MAC 地址;可为空,填了必须合法且在未删除数据中唯一。 */
     @Size(max = 17, message = "MAC地址长度不能超过17个字符")
     private String macAddress;
 
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java
deleted file mode 100644
index 0d4a9c9..0000000
--- a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package com.battery.business.domain.vo;
-
-/**
- * 可发现采集器信息。
- */
-public class DiscoveredCollectorVo
-{
-    /** 设备编码 */
-    private String deviceCode;
-
-    /** IP 地址 */
-    private String ip;
-
-    /** MAC 地址 */
-    private String mac;
-
-    /** 模拟器提供的采集器名称 */
-    private String name;
-
-    /** 制造商 */
-    private String manufacturer;
-
-    /** 型号 */
-    private String model;
-
-    /** 通道数 */
-    private Integer channelCount;
-
-    /** 在线状态 */
-    private String onlineStatus;
-
-    public DiscoveredCollectorVo()
-    {
-    }
-
-    public DiscoveredCollectorVo(String deviceCode, String ip, String mac, String name, String manufacturer,
-            String model, Integer channelCount, String onlineStatus)
-    {
-        this.deviceCode = deviceCode;
-        this.ip = ip;
-        this.mac = mac;
-        this.name = name;
-        this.manufacturer = manufacturer;
-        this.model = model;
-        this.channelCount = channelCount;
-        this.onlineStatus = onlineStatus;
-    }
-
-    public String getDeviceCode()
-    {
-        return deviceCode;
-    }
-
-    public void setDeviceCode(String deviceCode)
-    {
-        this.deviceCode = deviceCode;
-    }
-
-    public String getIp()
-    {
-        return ip;
-    }
-
-    public void setIp(String ip)
-    {
-        this.ip = ip;
-    }
-
-    public String getMac()
-    {
-        return mac;
-    }
-
-    public void setMac(String mac)
-    {
-        this.mac = mac;
-    }
-
-    public String getName()
-    {
-        return name;
-    }
-
-    public void setName(String name)
-    {
-        this.name = name;
-    }
-
-    public String getManufacturer()
-    {
-        return manufacturer;
-    }
-
-    public void setManufacturer(String manufacturer)
-    {
-        this.manufacturer = manufacturer;
-    }
-
-    public String getModel()
-    {
-        return model;
-    }
-
-    public void setModel(String model)
-    {
-        this.model = model;
-    }
-
-    public Integer getChannelCount()
-    {
-        return channelCount;
-    }
-
-    public void setChannelCount(Integer channelCount)
-    {
-        this.channelCount = channelCount;
-    }
-
-    public String getOnlineStatus()
-    {
-        return onlineStatus;
-    }
-
-    public void setOnlineStatus(String onlineStatus)
-    {
-        this.onlineStatus = onlineStatus;
-    }
-
-}
diff --git a/battery-business/src/main/java/com/battery/business/mapper/TCollectorMapper.java b/battery-business/src/main/java/com/battery/business/mapper/TCollectorMapper.java
index f6fdc15..6fd2d95 100644
--- a/battery-business/src/main/java/com/battery/business/mapper/TCollectorMapper.java
+++ b/battery-business/src/main/java/com/battery/business/mapper/TCollectorMapper.java
@@ -17,9 +17,6 @@
     /** 查询采集器列表。 */
     List<TCollector> selectCollectorList(TCollector collector);
 
-    /** 查询全部未删除采集器的设备编码。 */
-    List<String> selectRegisteredCollectorCodes();
-
     /** 根据设备编码查询未删除采集器。 */
     TCollector selectCollectorByCode(String collectorCode);
 
@@ -28,10 +25,6 @@
 
     /** 新增采集器。 */
     int insertCollector(TCollector collector);
-
-    /** 同步正式采集器的实时在线事实,不触碰软删除记录。 */
-    int updateRegisteredObservation(@Param("collectorCode") String collectorCode,
-            @Param("onlineStatus") String onlineStatus);
 
     /** 编辑采集器基础资料。 */
     int updateCollector(TCollector collector);
diff --git a/battery-business/src/main/java/com/battery/business/service/ICollectorDiscoveryService.java b/battery-business/src/main/java/com/battery/business/service/ICollectorDiscoveryService.java
deleted file mode 100644
index bd654c4..0000000
--- a/battery-business/src/main/java/com/battery/business/service/ICollectorDiscoveryService.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.battery.business.service;
-
-import java.util.List;
-import com.battery.business.domain.vo.DiscoveredCollectorVo;
-
-/**
- * 采集器设备发现服务。
- */
-public interface ICollectorDiscoveryService
-{
-    /**
-     * 查询当前可发现的采集器。
-     *
-     * @return 可发现采集器列表
-     */
-    List<DiscoveredCollectorVo> selectDiscoveredCollectorList();
-}
diff --git a/battery-business/src/main/java/com/battery/business/service/ITCollectorService.java b/battery-business/src/main/java/com/battery/business/service/ITCollectorService.java
index 03ed056..040c32e 100644
--- a/battery-business/src/main/java/com/battery/business/service/ITCollectorService.java
+++ b/battery-business/src/main/java/com/battery/business/service/ITCollectorService.java
@@ -1,7 +1,6 @@
 package com.battery.business.service;
 
 import java.util.List;
-import java.util.Set;
 import com.battery.business.domain.TCollector;
 
 /**
@@ -25,9 +24,6 @@
 
     /** 启用或禁用采集器。 */
     int updateCollectorStatus(TCollector collector);
-
-    /** 查询全部未删除采集器的设备编码。 */
-    Set<String> selectRegisteredCollectorCodes();
 
     /** 批量逻辑删除采集器。 */
     int deleteCollectorByIds(Long[] collectorIds, String updateBy);
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
index f988911..4dfdc59 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
@@ -9,6 +9,10 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
 import com.battery.device.api.DeviceAdapter;
 import com.battery.device.api.DeviceAdapterException;
 import com.battery.device.api.TestDataObservationAdapter;
@@ -46,6 +50,7 @@
 {
     /** 适配器类型按存储形态划分:这个形态读本项目的落地表(ADR-0065)。 */
     public static final String ADAPTER_TYPE = "PUSH_INGEST";
+    private static final Logger log = LoggerFactory.getLogger(PushIngestAdapter.class);
     /**
      * 身份映射(后端完成):22 所的存储形态没有设备类型与单元维度,一台电脑就是登记意义上的一个柜,
      * 通道号就是厂商自己的通道号(表名 {@code Ws<N>_IV_0} 里的 N)。厂商通道号进 {@code chlid},
@@ -155,14 +160,98 @@
     }
 
     /**
-     * 22 所的 IV 表里有温箱温度、压力与计数器,是现场唯一见到环境量的来源,
-     * 但类目语义与量纲尚未确认。按 ADR-0064 逐来源确认可得性,确认之前不交出测量值。
+     * 采集器辅助测量按 ADR-0064 由**同一来源实例**提供,所以这里读的是落地表里该来源自己那些通道的原始行。
+     *
+     * <p>哪些列是辅助测量、各叫什么指标、用什么单位、对应哪个采集器通道,全部只在
+     * 来源配置的 {@code auxiliaryMetrics} 里声明(例如
+     * {@code {"vendorChannel":24,"channelCode":"S-24","column":"Tempbox","metricCode":"TEMPERATURE","unit":"℃"}})。
+     * **没有声明就返回空值**:22 所 IV 表里确实有温箱温度、压力与计数器,是现场唯一见到环境量的来源,
+     * 但列语义与量纲尚未对着现场确认,未确认前不按列名推断语义、也不写出任何值。
+     * 声明之后不必改代码——确认结论落在配置里,不落在推断里。
+     *
+     * <p>来源不可达(心跳缺失或过期)时返回 {@code online=false} 且不带任何测量值:
+     * 离线不伪造测量值,业务侧据此不落任何采集器行。
      */
     @Override
     public CollectorRealtime latestCollector(String adapterCode, String collectorCode)
     {
-        return null;
+        List<AuxiliaryMetric> declared = auxiliaryMetrics();
+        if (declared.isEmpty() || !config.adapterCode().equals(adapterCode)) return null;
+        if (!heartbeatFresh())
+            return new CollectorRealtime(collectorCode, false, (int) PERSISTENCE_INTERVAL_MS, List.of(),
+                    config.timezoneId());
+        List<CollectorMeasurement> measurements = new ArrayList<>();
+        for (AuxiliaryMetric metric : declared)
+        {
+            TDeviceIngestRecord row = records.selectLatestSample(config.adapterCode(), DEVICE_TYPE, DEVICE_ID,
+                    SUB_DEVICE_ID, metric.vendorChannel());
+            if (row == null) continue;
+            BigDecimal value = payloadValue(row.getPayloadJson(), metric.column());
+            // 设备没给这一项就是没给:不写零、不写空值占位,也不阻断其他指标。
+            if (value == null) continue;
+            measurements.add(new CollectorMeasurement(metric.channelCode(),
+                    row.getRecordSequence() == null ? 0L : row.getRecordSequence(),
+                    row.getDeviceTimeUtc() == null ? null : row.getDeviceTimeUtc().toInstant(),
+                    row.getDeviceTimeRaw(),
+                    row.getReceivedAtUtc() == null ? null : row.getReceivedAtUtc().toInstant(),
+                    metric.metricCode(), value, metric.unit()));
+        }
+        return new CollectorRealtime(collectorCode, true, (int) PERSISTENCE_INTERVAL_MS, measurements,
+                config.timezoneId());
     }
+
+    /**
+     * 读取来源配置里声明的辅助测量列。配置格式非法时按"没有声明"处理并留一条警告:
+     * 宁可少交出一项测量,也不按猜测解释厂商列。
+     */
+    private List<AuxiliaryMetric> auxiliaryMetrics()
+    {
+        String declared = config.sourceOption("auxiliaryMetrics");
+        if (declared == null || declared.isBlank()) return List.of();
+        List<AuxiliaryMetric> metrics = new ArrayList<>();
+        try
+        {
+            for (Object item : JSON.parseArray(declared))
+            {
+                if (!(item instanceof JSONObject entry)) continue;
+                Integer vendorChannel = entry.getInteger("vendorChannel");
+                String channelCode = entry.getString("channelCode");
+                String column = entry.getString("column");
+                String metricCode = entry.getString("metricCode");
+                if (vendorChannel == null || channelCode == null || column == null || metricCode == null) continue;
+                metrics.add(new AuxiliaryMetric(vendorChannel, channelCode, column, metricCode,
+                        entry.getString("unit")));
+            }
+        }
+        catch (RuntimeException failure)
+        {
+            log.warn("来源 {} 的 auxiliaryMetrics 配置无法解析,按未声明处理", config.adapterCode(), failure);
+            return List.of();
+        }
+        return List.copyOf(metrics);
+    }
+
+    /** 从原始行里按声明取数值:非数值(含 null 与文本)一律当作"设备没给这一项"。 */
+    private static BigDecimal payloadValue(String payloadJson, String column)
+    {
+        if (payloadJson == null || payloadJson.isBlank()) return null;
+        try
+        {
+            Object value = JSON.parseObject(payloadJson).get(column);
+            if (value == null) return null;
+            if (value instanceof BigDecimal decimal) return decimal;
+            if (value instanceof Number number) return new BigDecimal(number.toString());
+            return null;
+        }
+        catch (RuntimeException failure)
+        {
+            return null;
+        }
+    }
+
+    /** 一条声明:来源侧列名到业务指标(含采集器通道与单位)的映射。 */
+    private record AuxiliaryMetric(int vendorChannel, String channelCode, String column, String metricCode,
+            String unit) { }
 
     /**
      * 22 所没有独立的工步层:工步与循环号只在采样行的 {@code Step_Index} / {@code Cycle_Index} 里,
@@ -223,16 +312,18 @@
     /** 心跳缺失即来源不可达:取数程序停了、或它自己连不上厂商库。 */
     private void requireFreshHeartbeat()
     {
-        java.util.Date receivedAt = records.selectHeartbeatAt(config.adapterCode());
-        if (receivedAt == null)
+        if (!heartbeatFresh())
             throw new DeviceAdapterException("SOURCE_OFFLINE",
-                    "尚未收到取数程序的推送:请确认 " + config.host()
+                    "尚未收到取数程序的推送或推送已超时:请确认 " + config.host()
                             + " 那台机器上的取数程序已启动,并指向本系统的接收端点", null);
-        Duration staleAfter = staleAfter();
-        Duration silent = Duration.between(receivedAt.toInstant(), clock.instant());
-        if (silent.compareTo(staleAfter) > 0)
-            throw new DeviceAdapterException("SOURCE_OFFLINE", "取数程序已停止推送:最近一次推送在 "
-                    + silent.toSeconds() + " 秒前,超过容忍窗口 " + staleAfter.toSeconds() + " 秒", null);
+    }
+
+    /** 来源是否在线:心跳行存在且在容忍窗口内。落地表随时可读,所以"能读到表"不能表示来源可达。 */
+    private boolean heartbeatFresh()
+    {
+        java.util.Date receivedAt = records.selectHeartbeatAt(config.adapterCode());
+        if (receivedAt == null) return false;
+        return Duration.between(receivedAt.toInstant(), clock.instant()).compareTo(staleAfter()) <= 0;
     }
 
     /** 容忍窗口按实例配置的轮询周期算:写库与推送都是这个节奏,连丢三轮即认为离线。 */
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
index 8cf4d0b..8edfbbe 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
@@ -3,22 +3,23 @@
 import java.net.InetAddress;
 import java.util.Date;
 import java.util.List;
-import java.util.Set;
 import java.util.regex.Pattern;
-import java.time.Instant;
-import java.util.Map;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.StringUtils;
 import com.battery.business.domain.TCollector;
+import com.battery.business.domain.TDeviceAdapter;
 import com.battery.business.mapper.TCollectorMapper;
+import com.battery.business.mapper.TDeviceAdapterMapper;
 import com.battery.business.service.ITCollectorService;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
 
 /**
  * 采集器 Service 实现。
+ *
+ * <p>采集器按 ADR-0064 不建独立设备模型:它的测量是「同一来源的辅助测量」,
+ * 由所链接的来源实例提供。因此这里不再走发现候选(真实来源根本不产出采集器候选),
+ * 而是由操作员手工登记来源身份,在线状态直接取所链接来源实例的连接结果。
  *
  * @author ruoyi
  */
@@ -28,7 +29,6 @@
     private static final String ENABLED = "0";
     private static final String DISABLED = "1";
     private static final String OFFLINE = "0";
-    private static final String ONLINE = "1";
     private static final String IDLE = "0";
     private static final String TESTING = "1";
     private static final Pattern IPV4_PATTERN = Pattern.compile(
@@ -36,12 +36,12 @@
     private static final Pattern MAC_PATTERN = Pattern.compile("^[0-9A-F]{2}(:[0-9A-F]{2}){5}$");
 
     private final TCollectorMapper collectorMapper;
-    private final DeviceDiscoveryCandidateStore candidateStore;
+    private final TDeviceAdapterMapper adapterMapper;
 
-    public TCollectorServiceImpl(TCollectorMapper collectorMapper, DeviceDiscoveryCandidateStore candidateStore)
+    public TCollectorServiceImpl(TCollectorMapper collectorMapper, TDeviceAdapterMapper adapterMapper)
     {
         this.collectorMapper = collectorMapper;
-        this.candidateStore = candidateStore;
+        this.adapterMapper = adapterMapper;
     }
 
     /**
@@ -59,7 +59,7 @@
         {
             throw new ServiceException("采集器不存在");
         }
-        return applyRealtimeState(collector, candidatesByCode());
+        return collector;
     }
 
     /**
@@ -77,32 +77,26 @@
         collector.setEndCreateTime(trimToNull(collector.getEndCreateTime()));
         String requestedOnlineStatus = collector.getOnlineStatus();
         validateOptionalStatus(requestedOnlineStatus, "在线状态");
+        // 在线状态由 SQL 从所链接来源实例的连接结果派生,这里不再从存储列过滤。
         collector.setOnlineStatus(null);
         validateOptionalStatus(collector.getTestStatus(), "测试状态");
         validateOptionalStatus(collector.getStatus(), "状态");
-        Map<String, CollectorCandidate> candidates = candidatesByCode();
-        return collectorMapper.selectCollectorList(collector).stream()
-                .map(item -> applyRealtimeState(item, candidates))
-                .filter(item -> StringUtils.isEmpty(requestedOnlineStatus)
-                        || requestedOnlineStatus.equals(item.getOnlineStatus()))
-                .toList();
+        return collectorMapper.selectCollectorList(collector);
     }
 
     /**
      * 新增采集器并初始化运行状态。
+     *
+     * <p>前置条件:设备编码与名称非空且设备编码未登记过;IP 与 MAC 可留空(辅助测量不是独立硬件,
+     * 系统不连它),填了则校验格式。在线状态取所链接来源实例的连接结果,未链接来源时保持离线。
      */
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int insertCollector(TCollector collector)
     {
         if (collector == null) throw new ServiceException("采集器信息不能为空");
-        collector.setCollectorCode(trimToNull(collector.getCollectorCode()));
-        CollectorCandidate candidate = candidateStore.findCollector(collector.getCollectorCode(), Instant.now())
-                .orElseThrow(() -> new ServiceException("请先选择有效的发现候选设备"));
-        collector.setIpAddress(candidate.ip());
-        collector.setMacAddress(candidate.mac());
-        collector.setChannelCount(candidate.channelCount());
         validateCollector(collector, false);
+        requireLinkedAdapter(collector);
         collector.setOnlineStatus(OFFLINE);
         // 测试占用由 t_test_collector_channel + t_test 派生,采集器登记表不写入这些字段。
         collector.setTestStatus(IDLE);
@@ -118,11 +112,8 @@
         }
         collector.setDelFlag("0");
         collector.setCreateTime(new Date());
-        int rows = collectorMapper.insertCollector(collector);
-        // 登记只确认来源身份,在线与离线事实都按候选原样同步。
-        collectorMapper.updateRegisteredObservation(collector.getCollectorCode(),
-                candidate.online() ? "ONLINE" : "OFFLINE");
-        return rows;
+        // 登记只确认来源身份:在线事实由所链接来源实例的连接结果表达,登记时不改写它。
+        return collectorMapper.insertCollector(collector);
     }
 
     /**
@@ -136,7 +127,10 @@
         if (current == null) throw new ServiceException("采集器不存在");
         if (collector.getCollectorCode() != null && !collector.getCollectorCode().trim().equals(current.getCollectorCode()))
             throw new ServiceException("设备编码不可修改");
+        // 编辑只提交可改字段:未提交的 IP/MAC 不能被当成"清空"。
+        fillMissingFromCurrent(collector, current);
         validateCollector(collector, true);
+        requireLinkedAdapter(collector);
         validateStatus(collector.getStatus(), "状态");
         if (DISABLED.equals(collector.getStatus()))
         {
@@ -168,13 +162,6 @@
         }
         collector.setUpdateTime(new Date());
         return collectorMapper.updateCollectorStatus(collector);
-    }
-
-    /** 查询全部未删除采集器的设备编码,供发现候选排除已登记设备。 */
-    @Override
-    public Set<String> selectRegisteredCollectorCodes()
-    {
-        return Set.copyOf(collectorMapper.selectRegisteredCollectorCodes());
     }
 
     /**
@@ -215,7 +202,7 @@
         }
         collector.setCollectorCode(trimToNull(collector.getCollectorCode()));
         collector.setIpAddress(trimToNull(collector.getIpAddress()));
-        collector.setMacAddress(normalizeMacAddress(collector.getMacAddress()));
+        collector.setMacAddress(normalizeOptionalMacAddress(collector.getMacAddress()));
         collector.setCollectorName(trimToNull(collector.getCollectorName()));
         collector.setManufacturer(trimToNull(collector.getManufacturer()));
         collector.setModel(trimToNull(collector.getModel()));
@@ -229,7 +216,7 @@
         {
             throw new ServiceException("采集器名称不能为空");
         }
-        validateIpAddress(collector.getIpAddress());
+        validateOptionalIpAddress(collector.getIpAddress());
         if (collector.getChannelCount() == null || collector.getChannelCount() < 1
                 || collector.getChannelCount() > 10000)
         {
@@ -238,8 +225,34 @@
         validateUnique(collector);
     }
 
+    /** 编辑时用既有值补齐未提交的字段,避免把"没传"当成"清空"。 */
+    private void fillMissingFromCurrent(TCollector collector, TCollector current)
+    {
+        if (collector.getCollectorCode() == null) collector.setCollectorCode(current.getCollectorCode());
+        if (collector.getIpAddress() == null) collector.setIpAddress(current.getIpAddress());
+        if (collector.getMacAddress() == null) collector.setMacAddress(current.getMacAddress());
+        if (collector.getCollectorName() == null) collector.setCollectorName(current.getCollectorName());
+        if (collector.getChannelCount() == null) collector.setChannelCount(current.getChannelCount());
+        if (collector.getStatus() == null) collector.setStatus(current.getStatus());
+        if (collector.getAdapterId() == null) collector.setAdapterId(current.getAdapterId());
+    }
+
+    /** 校验所链接的来源实例存在;来源是采集器测量的提供方,链接后可读它的连接结果。 */
+    private void requireLinkedAdapter(TCollector collector)
+    {
+        if (collector.getAdapterId() == null)
+        {
+            return;
+        }
+        TDeviceAdapter adapter = adapterMapper.selectDeviceAdapterById(collector.getAdapterId());
+        if (adapter == null)
+        {
+            throw new ServiceException("所链接的数据来源不存在");
+        }
+    }
+
     /**
-     * 校验设备编码和 MAC 地址在未删除数据中唯一。
+     * 校验设备编码唯一;MAC 地址只在填写时参与唯一性。
      */
     private void validateUnique(TCollector collector)
     {
@@ -247,6 +260,10 @@
         if (isDifferentCollector(sameCode, collector.getCollectorId()))
         {
             throw new ServiceException("设备编码已存在");
+        }
+        if (collector.getMacAddress() == null)
+        {
+            return;
         }
         TCollector sameMac = collectorMapper.selectCollectorByMacAddress(collector.getMacAddress());
         if (isDifferentCollector(sameMac, collector.getCollectorId()))
@@ -264,14 +281,14 @@
     }
 
     /**
-     * 规范化并校验 MAC 地址。
+     * 规范化并校验 MAC 地址;未填写时保持为空。
      */
-    private String normalizeMacAddress(String macAddress)
+    private String normalizeOptionalMacAddress(String macAddress)
     {
         String normalized = trimToNull(macAddress);
         if (normalized == null)
         {
-            throw new ServiceException("MAC地址不能为空");
+            return null;
         }
         normalized = normalized.replace('-', ':').toUpperCase();
         if (!MAC_PATTERN.matcher(normalized).matches())
@@ -282,13 +299,13 @@
     }
 
     /**
-     * 校验 IPv4 或 IPv6 地址,不接受主机名。
+     * 校验 IPv4 或 IPv6 地址,不接受主机名;未填写时跳过。
      */
-    private void validateIpAddress(String ipAddress)
+    private void validateOptionalIpAddress(String ipAddress)
     {
         if (StringUtils.isEmpty(ipAddress))
         {
-            throw new ServiceException("IP地址不能为空");
+            return;
         }
         if (IPV4_PATTERN.matcher(ipAddress).matches())
         {
@@ -351,20 +368,5 @@
     private String trimToNull(String value)
     {
         return StringUtils.isEmpty(StringUtils.trim(value)) ? null : StringUtils.trim(value);
-    }
-
-    private Map<String, CollectorCandidate> candidatesByCode()
-    {
-        return candidateStore.snapshot(Instant.now()).collectors().stream()
-                .collect(java.util.stream.Collectors.toMap(CollectorCandidate::deviceCode, item -> item));
-    }
-
-    private TCollector applyRealtimeState(TCollector collector, Map<String, CollectorCandidate> candidates)
-    {
-        CollectorCandidate candidate = candidates.get(collector.getCollectorCode());
-        collector.setOnlineStatus(candidate != null && candidate.online() ? ONLINE : OFFLINE);
-        // 候选过期只改变当前在线展示,不能抹掉 MySQL 保存的最近观测时间。
-        if (candidate != null) collector.setLastSeenAtUtc(candidate.observedAt());
-        return collector;
     }
 }
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
index f9a7270..3ef92c6 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
@@ -18,10 +18,12 @@
 import com.battery.device.api.model.Measurement;
 import com.battery.business.domain.TTest;
 import com.battery.business.domain.TTestBattery;
+import com.battery.business.domain.TDeviceAdapter;
 import com.battery.business.service.ITestLiveService;
 import com.battery.business.service.ITestLiveService.TestPlanRow;
 import com.battery.business.service.ITestLiveService.TestPlanSnapshot;
 import com.battery.business.service.ITTestService;
+import com.battery.business.mapper.TDeviceAdapterMapper;
 import com.battery.business.mapper.TTestMapper;
 
 /** 将设备最新快照转换为测试视角,按测试接收状态决定是否展示采集参数。 */
@@ -32,15 +34,16 @@
     private final DeviceDiscoveryCandidateStore candidates;
     private final TestDataObservationAdapter adapter;
     private final TTestMapper mapper;
+    private final TDeviceAdapterMapper adapters;
 
     @Autowired
     public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
-            @Nullable TestDataObservationAdapter adapter, TTestMapper mapper)
-    { this.tests = tests; this.candidates = candidates; this.adapter = adapter; this.mapper = mapper; }
+            @Nullable TestDataObservationAdapter adapter, TTestMapper mapper, TDeviceAdapterMapper adapters)
+    { this.tests = tests; this.candidates = candidates; this.adapter = adapter; this.mapper = mapper; this.adapters = adapters; }
 
     public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
             TestDataObservationAdapter adapter)
-    { this(tests, candidates, adapter, null); }
+    { this(tests, candidates, adapter, null, null); }
 
     /** 返回测试当前设备事实;接收中展示测量和实时提醒,历史异常处理状态不参与判定。 */
     @Override public LiveState current(Long testId)
@@ -223,45 +226,66 @@
         return value;
     }
 
+    /**
+     * 采集器来源:测量按 ADR-0064 由同一来源实例作为辅助测量提供,所以它的在线事实就是该来源的可达性
+     * (ADR-0063:在线只表示来源可达),不再依赖只有开发模拟器会产出的发现候选。
+     *
+     * <p>来源不提供辅助测量、或来源可达但这一刻还没有任何辅助测量值时,明确标注为「无辅助数据」:
+     * 这是正常取值而不是故障,主测量接收不受影响,也不因此生成离线异常。来源不可达时不标这个标记——
+     * 那种情况是来源离线,由既有的离线异常路径表达。
+     */
     private LiveSource collectorSource(TTestBattery battery, boolean receiving)
     {
         if (adapter == null || mapper == null) return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
                 "OFFLINE", Map.of());
         String collectorCode = mapper.selectCollectorCode(battery.getCollectorId());
-        // 非接收状态只读设备发现事实,不能先请求测量再隐藏,否则仍会产生新观测。
+        String adapterCode = battery.getBinding() == null ? null : battery.getBinding().getAdapterCode();
+        boolean sourceOnline = sourceOnline(adapterCode);
+        // 非接收状态只读来源可达性,不能先请求测量再隐藏,否则仍会产生新观测。
         if (!receiving)
         {
-            boolean online = candidates.findCollector(collectorCode, Instant.now())
-                    .map(DeviceDiscoveryCandidateStore.CollectorCandidate::online).orElse(false);
             return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
-                    online ? "ONLINE" : "OFFLINE", Map.of());
+                    sourceOnline ? "ONLINE" : "OFFLINE", Map.of());
         }
-        var collector = adapter.latestCollector(
-                battery.getBinding() == null ? null : battery.getBinding().getAdapterCode(), collectorCode);
-        if (collector != null)
+        var collector = adapter.latestCollector(adapterCode, collectorCode);
+        Map<String, Object> value = new LinkedHashMap<>();
+        value.put("source", "采集器");
+        if (collector == null)
         {
-            Map<String, Object> value = new LinkedHashMap<>();
-            value.put("source", "采集器"); value.put("sampleIntervalMs", collector.sampleIntervalMs());
-            // 在业务响应边界转换毫秒时间,设备时间与服务器接收时间分别保留。
-            value.put("measurements", collector.measurements().stream().filter(item -> battery.getCollectorChannels() != null
-                    && battery.getCollectorChannels().stream().anyMatch(binding -> binding.getChannelCode().equals(item.channelCode())
-                            && TestWarningExceptionService.matchesCollectorMetric(binding.getMeasurementCode(), item.metricCode()))).map(item -> {
-                Map<String, Object> measurement = new LinkedHashMap<>();
-                measurement.put("metricCode", item.metricCode());
-                measurement.put("channelCode", item.channelCode());
-                measurement.put("value", item.value());
-                measurement.put("unit", item.unit());
-                measurement.put("deviceTime", item.deviceTime().toEpochMilli());
-                measurement.put("serverReceivedAt", item.serverReceivedAt().toEpochMilli());
-                return measurement;
-            }).toList());
-            value.put("units", collector.measurements().stream().map(item -> item.unit()).distinct().toList());
-            collector.measurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo)
-                    .ifPresent(observedAt -> value.put("observedAt", observedAt.toEpochMilli()));
+            // 该来源根本不提供辅助测量(新威实测无辅助数据;推式来源的列语义确认前同样如此)。
+            if (sourceOnline) value.put("auxiliaryDataAvailable", false);
             return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
-                    collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
+                    sourceOnline ? "ONLINE" : "OFFLINE", value);
         }
-        return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(), "OFFLINE", Map.of());
+        value.put("sampleIntervalMs", collector.sampleIntervalMs());
+        // 在业务响应边界转换毫秒时间,设备时间与服务器接收时间分别保留。
+        value.put("measurements", collector.measurements().stream().filter(item -> battery.getCollectorChannels() != null
+                && battery.getCollectorChannels().stream().anyMatch(binding -> binding.getChannelCode().equals(item.channelCode())
+                        && TestWarningExceptionService.matchesCollectorMetric(binding.getMeasurementCode(), item.metricCode()))).map(item -> {
+            Map<String, Object> measurement = new LinkedHashMap<>();
+            measurement.put("metricCode", item.metricCode());
+            measurement.put("channelCode", item.channelCode());
+            measurement.put("value", item.value());
+            measurement.put("unit", item.unit());
+            measurement.put("deviceTime", item.deviceTime().toEpochMilli());
+            measurement.put("serverReceivedAt", item.serverReceivedAt().toEpochMilli());
+            return measurement;
+        }).toList());
+        value.put("units", collector.measurements().stream().map(item -> item.unit()).distinct().toList());
+        collector.measurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo)
+                .ifPresent(observedAt -> value.put("observedAt", observedAt.toEpochMilli()));
+        // 来源可达但这一刻还没有任何辅助测量值:同样是"无辅助数据",按空值展示而不是占位。
+        if (collector.online() && collector.measurements().isEmpty()) value.put("auxiliaryDataAvailable", false);
+        return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
+                collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
+    }
+
+    /** 来源实例是否可达;来源编码缺失或查询不到时按不可达处理,不猜也不回退到其他实例。 */
+    private boolean sourceOnline(String adapterCode)
+    {
+        if (adapters == null || adapterCode == null || adapterCode.isBlank()) return false;
+        TDeviceAdapter adapter = adapters.selectDeviceAdapterByCode(adapterCode);
+        return adapter != null && "ONLINE".equalsIgnoreCase(adapter.getConnectionStatus());
     }
 
 }
diff --git a/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml b/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
index dce3675..71fde52 100644
--- a/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
@@ -28,10 +28,20 @@
                     select="com.battery.business.mapper.DeviceRelatedTestMapper.selectCollectorRelatedTests" />
     </resultMap>
 
+    <!-- 在线状态取所链接来源实例的连接结果(ADR-0063:在线只表示来源可达);
+         采集器测量由该来源提供,所以它不是独立可达的硬件。SELECT 与 WHERE 各用一次,故抽成片段。 -->
+    <sql id="derivedOnlineStatus">
+        case when exists (
+            select 1 from t_device_adapter adapter_fact
+            where adapter_fact.adapter_id = c.adapter_id
+              and upper(adapter_fact.connection_status) = 'ONLINE'
+        ) then '1' else '0' end
+    </sql>
+
     <sql id="selectCollectorVo">
         select c.collector_id, c.collector_code, c.adapter_id, c.ip_address, c.mac_address, c.collector_name,
                c.manufacturer, c.model, c.channel_count,
-               case when upper(c.online_status) = 'ONLINE' or c.online_status = '1' then '1' else '0' end as online_status,
+               <include refid="derivedOnlineStatus" /> as online_status,
                c.last_seen_at_utc,
                case when exists (
                    select 1 from t_test_collector_channel mapping
@@ -68,7 +78,8 @@
                 and create_time &lt;= #{endCreateTime}
             </if>
             <if test="onlineStatus != null and onlineStatus != ''">
-                and online_status = #{onlineStatus}
+                <!-- WHERE 里不能用 SELECT 别名(MySQL 只在 HAVING 认得),所以按同一表达式再过一遍。 -->
+                and (<include refid="derivedOnlineStatus" />) = #{onlineStatus}
             </if>
             <if test="testStatus != null and testStatus != ''">
                 and (case when exists (
@@ -84,12 +95,6 @@
             </if>
         </where>
         order by create_time desc, collector_id desc
-    </select>
-
-    <select id="selectRegisteredCollectorCodes" resultType="String">
-        select collector_code
-        from t_collector
-        where del_flag = '0'
     </select>
 
     <select id="selectCollectorByCode" parameterType="String" resultMap="TCollectorResult">
@@ -108,27 +113,21 @@
 
     <insert id="insertCollector" parameterType="TCollector" useGeneratedKeys="true" keyProperty="collectorId">
         insert into t_collector
-        (collector_code, ip_address, mac_address, collector_name, manufacturer, model,
+        (collector_code, adapter_id, ip_address, mac_address, collector_name, manufacturer, model,
          channel_count, online_status, last_seen_at_utc,
          status, del_flag, create_by, create_time, remark)
         values
-        (#{collectorCode}, #{ipAddress}, #{macAddress}, #{collectorName}, #{manufacturer}, #{model},
+        (#{collectorCode}, #{adapterId}, #{ipAddress}, #{macAddress}, #{collectorName}, #{manufacturer}, #{model},
          #{channelCount}, case when #{onlineStatus} in ('1', 'ONLINE') then 'ONLINE' else 'OFFLINE' end,
          #{lastSeenAtUtc},
          #{status}, #{delFlag}, #{createBy}, #{createTime}, #{remark})
     </insert>
 
-    <update id="updateRegisteredObservation">
-        update t_collector
-        set online_status = case when #{onlineStatus} in ('1', 'ONLINE') then 'ONLINE' else 'OFFLINE' end,
-            last_seen_at_utc = utc_timestamp(3), update_time = sysdate()
-        where collector_code = #{collectorCode} and del_flag = 0
-    </update>
-
     <update id="updateCollector" parameterType="TCollector">
         update t_collector
         <set>
             collector_code = #{collectorCode},
+            adapter_id = #{adapterId},
             ip_address = #{ipAddress},
             mac_address = #{macAddress},
             collector_name = #{collectorName},
diff --git a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
index 25a2792..ec6c1be 100644
--- a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
+++ b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
@@ -50,6 +50,13 @@
     private final FakeIngestLanding landing = new FakeIngestLanding();
     private final TDeviceAdapterMapper adapters = Mockito.mock(TDeviceAdapterMapper.class);
     private static final Instant NOW = Instant.parse("2026-09-27T09:00:00Z");
+    /**
+     * 环境量的确认结论落在这里:厂商列名只在配置里出现一次,代码里没有任何厂商列名。
+     * 22 所的列语义与量纲仍未现场确认,所以生产配置里默认没有这一项。
+     */
+    private static final Map<String, String> AUXILIARY_METRICS = Map.of("auxiliaryMetrics",
+            "[{\"vendorChannel\":26,\"channelCode\":\"S-01\",\"column\":\"Tempbox\","
+                    + "\"metricCode\":\"TEMPERATURE\",\"unit\":\"℃\"}]");
 
     @Test
     void topologyPushMakesChannelsCandidatesWithTheirOwnLatestDataTime()
@@ -163,9 +170,9 @@
         assertTrue(adapter.inquire(Set.of(channel())).isEmpty());
     }
 
-    /** 采集器辅助测量按 ADR-0064 逐来源确认;22 所环境量的列语义未确认,因此不交出测量值。 */
+    /** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */
     @Test
-    void collectorAndStepCycleObservationsStayEmptyForThisStorageForm()
+    void collectorAndStepCycleObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics()
     {
         ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
         ingest(BATCH);
@@ -178,6 +185,61 @@
         assertEquals(5000L, adapter.persistenceIntervalMillis(channel()));
         AdapterCapabilities capabilities = adapter.capabilities();
         assertTrue(capabilities.discovery() && capabilities.realtimeObservation() && !capabilities.control());
+    }
+
+    /** 环境量与量纲确认后落到配置:列名、指标编码、单位、采集器通道都由声明给出,代码里没有厂商列名。 */
+    @Test
+    void declaredAuxiliaryMetricIsProjectedWithItsOwnSourceIdentityValueUnitAndDeviceTime()
+    {
+        ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
+        ingest("""
+                {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
+                  {"record_time":"134349692000000000","Data_Point":420,"Voltage":1.26778,"eCurrent":0.5,"Tempbox":26.5}]}
+                """);
+        PushIngestAdapter adapter = adapter(NOW, AUXILIARY_METRICS);
+
+        var collector = adapter.latestCollector(ADAPTER, "collector-1");
+
+        assertTrue(collector.online());
+        var measurement = collector.measurements().get(0);
+        assertEquals("S-01", measurement.channelCode());
+        assertEquals("TEMPERATURE", measurement.metricCode());
+        assertEquals(0, new java.math.BigDecimal("26.5").compareTo(measurement.value()));
+        assertEquals("℃", measurement.unit());
+        // 设备时间与来源身份来自这一行自己:不与检测柜记录共用时间,也不伪造配对。
+        assertEquals(Instant.parse("2026-09-27T07:53:20Z"), measurement.deviceTime());
+        assertEquals("134349692000000000", measurement.deviceTimeRaw());
+    }
+
+    /** 落地行里没有这一列(设备没给)就不写这一项,也不写零占位。 */
+    @Test
+    void declaredAuxiliaryMetricAbsentFromTheRowIsNotFabricated()
+    {
+        ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
+        ingest(BATCH);
+        PushIngestAdapter adapter = adapter(NOW, AUXILIARY_METRICS);
+
+        var collector = adapter.latestCollector(ADAPTER, "collector-1");
+
+        assertTrue(collector.online());
+        assertTrue(collector.measurements().isEmpty());
+    }
+
+    /** 来源离线(心跳超时)时不交出任何测量值:离线不伪造测量值,业务侧据此也不落采集器行。 */
+    @Test
+    void staleHeartbeatReportsCollectorSourceOfflineWithoutMeasurements()
+    {
+        ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
+        ingest("""
+                {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
+                  {"record_time":"134349692000000000","Data_Point":420,"Voltage":1.26778,"eCurrent":0.5,"Tempbox":26.5}]}
+                """);
+        PushIngestAdapter adapter = adapter(NOW.plus(Duration.ofMinutes(10)), AUXILIARY_METRICS);
+
+        var collector = adapter.latestCollector(ADAPTER, "collector-1");
+
+        assertFalse(collector.online());
+        assertTrue(collector.measurements().isEmpty());
     }
 
     @Test
@@ -198,14 +260,18 @@
 
     private PushIngestAdapter adapter(Instant now)
     {
-        return new PushIngestAdapter(config(), landing, Clock.fixed(now, ZoneOffset.UTC));
+        return adapter(now, Map.of("tokenEnv", "BATTERY_INGEST_TOKEN"));
     }
 
-    private static AdapterInstanceConfig config()
+    private PushIngestAdapter adapter(Instant now, Map<String, String> sourceOptions)
+    {
+        return new PushIngestAdapter(config(sourceOptions), landing, Clock.fixed(now, ZoneOffset.UTC));
+    }
+
+    private static AdapterInstanceConfig config(Map<String, String> sourceOptions)
     {
         return new AdapterInstanceConfig(ADAPTER, PushIngestAdapter.ADAPTER_TYPE, "127.0.0.1", 8080, 60000,
-                "Asia/Shanghai", true, AdapterCapabilities.observeOnly(),
-                Map.of("tokenEnv", "BATTERY_INGEST_TOKEN"));
+                "Asia/Shanghai", true, AdapterCapabilities.observeOnly(), sourceOptions);
     }
 
     private void ingest(String body)
diff --git a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestReceptionContractTest.java b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestReceptionContractTest.java
index 5f83d84..fa11347 100644
--- a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestReceptionContractTest.java
+++ b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestReceptionContractTest.java
@@ -50,6 +50,16 @@
               {"record_time":"134349698000000000","Data_Point":422,"Step_Time":600.0,"Step_Index":4,
                "Cycle_Index":1,"Voltage":1.2702,"eCurrent":0.5,"ChargeCapacity":0.122,"ChargeEnergy":0.32}]}
             """;
+    /** 同一批里带上已声明的环境量列:辅助测量的值取自这一行自己,不做任何缩放。 */
+    private static final String AUXILIARY_BATCH = """
+            {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
+              {"record_time":"134349698000000000","Data_Point":421,"Step_Time":600.0,"Step_Index":4,
+               "Cycle_Index":1,"Voltage":1.2701,"eCurrent":0.5,"ChargeCapacity":0.121,"ChargeEnergy":0.31,
+               "Tempbox":26.5},
+              {"record_time":"134349704000000000","Data_Point":422,"Step_Time":660.0,"Step_Index":4,
+               "Cycle_Index":1,"Voltage":1.2702,"eCurrent":0.5,"ChargeCapacity":0.122,"ChargeEnergy":0.32,
+               "Tempbox":26.5}]}
+            """;
 
     private final FakeIngestLanding landing = new FakeIngestLanding();
     private final TDeviceAdapterMapper adapters = Mockito.mock(TDeviceAdapterMapper.class);
@@ -100,19 +110,78 @@
         verify(mapper, times(2)).insertRealtimeRecord(any());
     }
 
+    /**
+     * 采集器辅助测量按 ADR-0064 由同一来源提供:它作为扩展测量记录进入实时记录,
+     * **不跟检测柜记录伪造配对**——检测柜走各自的设备数据行(槽位为空),采集器走轮询槽位行(柜端字段为空),
+     * 两边各自保留自己的设备时间,绑定角色决定它进哪个指标。
+     */
+    @Test
+    void collectorAuxiliaryMeasurementBecomesItsOwnRealtimeRowWithoutFakingAPairing()
+    {
+        PushIngestAdapter adapter = ingestPushSource("{\"tokenEnv\":\"BATTERY_INGEST_TOKEN\",\"auxiliaryMetrics\":"
+                + "[{\"vendorChannel\":26,\"channelCode\":\"S-01\",\"column\":\"Tempbox\","
+                + "\"metricCode\":\"TEMPERATURE\",\"unit\":\"℃\"}]}");
+        TTestMapper mapper = mock(TTestMapper.class);
+        ITTestService tests = mock(ITTestService.class);
+        TTest test = receivingTest();
+        TTestBattery battery = test.getBatteries().get(0);
+        battery.setCollectorId(7L);
+        com.battery.business.domain.TTestCollectorChannel collectorChannel =
+                new com.battery.business.domain.TTestCollectorChannel();
+        collectorChannel.setChannelCode("S-01");
+        collectorChannel.setMeasurementCode("PRIMARY_TEMPERATURE");
+        battery.setCollectorChannels(List.of(collectorChannel));
+        when(mapper.selectReceivingTestIds()).thenReturn(List.of(1L));
+        when(mapper.selectTestByIdForUpdate(1L)).thenReturn(test);
+        when(tests.selectTestById(1L)).thenReturn(test);
+        when(mapper.selectCollectorCode(7L)).thenReturn("j22-collector");
+        TestDataReceptionServiceImpl service = new TestDataReceptionServiceImpl(mapper, tests, adapter, clock);
+
+        landing.insertIngestRecord(boundaryRow("DB_20260927_155209/Ws26_IV_0:420", 420L));
+        service.captureReceivedData();
+        ingest(AUXILIARY_BATCH);
+        clock.advance(Duration.ofSeconds(60));
+        service.captureReceivedData();
+
+        ArgumentCaptor<TTestRealtimeRecord> rows = ArgumentCaptor.forClass(TTestRealtimeRecord.class);
+        verify(mapper, times(3)).insertRealtimeRecord(rows.capture());
+        List<TTestRealtimeRecord> cabinetRows = rows.getAllValues().subList(0, 2);
+        TTestRealtimeRecord collectorRow = rows.getAllValues().get(2);
+        // 检测柜事实不占轮询槽位,采集器事实占槽位:两条路径互不覆盖。
+        assertTrue(cabinetRows.stream().allMatch(row -> row.getCaptureSlotUtc() == null));
+        assertTrue(collectorRow.getCaptureSlotUtc() != null);
+        // 采集器行不携带检测柜设备时间与检测柜身份,来源行身份里也没有 cabinet 段。
+        assertEquals(null, collectorRow.getCabinetDeviceTimeUtc());
+        assertEquals(null, collectorRow.getCabinetChlid());
+        assertTrue(collectorRow.getSourceRecordId().indexOf("cabinet:") < 0);
+        // 测量值取自这一行自己的设备时间(该通道落地表里最新一行),指标编码与单位按配置声明,
+        // 固定指标落到自己的列。
+        assertEquals(Instant.parse("2026-09-27T08:13:20Z"), collectorRow.getCollectorDeviceTimeUtc().toInstant());
+        assertEquals("S-01", collectorRow.getCollectorChannelCode());
+        assertEquals(0, new java.math.BigDecimal("26.5").compareTo(collectorRow.getTemperature()));
+        assertEquals(Long.valueOf(7L), collectorRow.getCollectorId());
+    }
+
     private PushIngestAdapter ingestPushSource()
+    {
+        return ingestPushSource("{\"tokenEnv\":\"BATTERY_INGEST_TOKEN\"}");
+    }
+
+    private PushIngestAdapter ingestPushSource(String sourceConfig)
     {
         TDeviceAdapter source = new TDeviceAdapter();
         source.setAdapterCode(ADAPTER);
         source.setAdapterType(PushIngestAdapter.ADAPTER_TYPE);
         source.setStatus("0");
         source.setTimezoneId("Asia/Shanghai");
-        source.setSourceConfig("{\"tokenEnv\":\"BATTERY_INGEST_TOKEN\"}");
+        source.setSourceConfig(sourceConfig);
         when(adapters.selectDeviceAdapterByCode(ADAPTER)).thenReturn(source);
         ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
         return new PushIngestAdapter(new AdapterInstanceConfig(ADAPTER, PushIngestAdapter.ADAPTER_TYPE,
                 "127.0.0.1", 8080, 60000, "Asia/Shanghai", true, AdapterCapabilities.observeOnly(),
-                java.util.Map.of("tokenEnv", "BATTERY_INGEST_TOKEN")), landing, clock);
+                com.alibaba.fastjson2.JSON.parseObject(sourceConfig)
+                        .toJavaObject(new com.alibaba.fastjson2.TypeReference<java.util.Map<String, String>>() { })),
+                landing, clock);
     }
 
     private void ingest(String body)
diff --git a/battery-device-api/src/main/java/com/battery/device/api/discovery/DeviceDiscoveryCandidateStore.java b/battery-device-api/src/main/java/com/battery/device/api/discovery/DeviceDiscoveryCandidateStore.java
index 7bb842c..360c126 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/discovery/DeviceDiscoveryCandidateStore.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/discovery/DeviceDiscoveryCandidateStore.java
@@ -13,9 +13,6 @@
     /** upsert 本轮检测柜观测,未返回设备保留到逻辑过期。 */
     void recordCabinets(List<CabinetCandidate> candidates);
 
-    /** 替换指定适配器的采集器观测,使已删除模拟采集器立即消失。 */
-    void replaceCollectors(String adapterCode, List<CollectorCandidate> candidates);
-
     /** 删除指定适配器的全部发现候选;仅供人工重置或实机联调清理。 */
     void removeAdapter(String adapterCode);
 
@@ -28,18 +25,11 @@
         return snapshot(now).cabinets().stream().filter(item -> item.deviceCode().equals(deviceCode)).findFirst();
     }
 
-    /** 按设备编码查询指定时刻未过期的采集器观测;在线和登记状态由业务层校验。 */
-    default Optional<CollectorCandidate> findCollector(String deviceCode, Instant now)
-    {
-        return snapshot(now).collectors().stream().filter(item -> item.deviceCode().equals(deviceCode)).findFirst();
-    }
-
-    record CandidateSnapshot(List<CabinetCandidate> cabinets, List<CollectorCandidate> collectors)
+    record CandidateSnapshot(List<CabinetCandidate> cabinets)
     {
         public CandidateSnapshot
         {
             cabinets = List.copyOf(cabinets);
-            collectors = List.copyOf(collectors);
         }
     }
 
@@ -93,8 +83,4 @@
             this(identity, online, workStatus, observedAt, latestSnapshot, lastDataAt, null);
         }
     }
-
-    record CollectorCandidate(String adapterCode, String deviceCode, String ip, String mac, String name,
-            String manufacturer, String model, int channelCount, boolean online,
-            Instant observedAt, Instant expiresAt) { }
 }
diff --git a/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java b/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
index 2426a24..da84c61 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
@@ -9,7 +9,6 @@
 public final class InMemoryDeviceDiscoveryCandidateStore implements DeviceDiscoveryCandidateStore
 {
     private final Map<String, CabinetCandidate> cabinets = new LinkedHashMap<>();
-    private final Map<String, CollectorCandidate> collectors = new LinkedHashMap<>();
 
     /** 按设备编码 upsert 本轮检测柜观测;本轮未返回的候选保留到逻辑过期。 */
     @Override
@@ -18,20 +17,11 @@
         candidates.forEach(item -> cabinets.put(item.deviceCode(), item));
     }
 
-    /** 整体替换指定适配器的采集器候选,使该适配器下已不存在的采集器立即消失。 */
-    @Override
-    public synchronized void replaceCollectors(String adapterCode, List<CollectorCandidate> candidates)
-    {
-        collectors.values().removeIf(item -> item.adapterCode().equals(adapterCode));
-        candidates.forEach(item -> collectors.put(item.deviceCode(), item));
-    }
-
-    /** 删除指定适配器名下的检测柜与采集器候选;仅供人工重置或联调清理。 */
+    /** 删除指定适配器名下的检测柜候选;仅供人工重置或联调清理。 */
     @Override
     public synchronized void removeAdapter(String adapterCode)
     {
         cabinets.values().removeIf(item -> item.adapterCode().equals(adapterCode));
-        collectors.values().removeIf(item -> item.adapterCode().equals(adapterCode));
     }
 
     /** 返回指定时刻尚未过期的候选,并清理已过期条目。 */
@@ -39,7 +29,6 @@
     public synchronized CandidateSnapshot snapshot(Instant now)
     {
         cabinets.values().removeIf(item -> !item.expiresAt().isAfter(now));
-        collectors.values().removeIf(item -> !item.expiresAt().isAfter(now));
-        return new CandidateSnapshot(List.copyOf(cabinets.values()), List.copyOf(collectors.values()));
+        return new CandidateSnapshot(List.copyOf(cabinets.values()));
     }
 }
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
index d122581..b39bd92 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
@@ -67,6 +67,11 @@
     /** 工步层的共用读取窗口:足以覆盖一轮接收里工步与循环的连续调用,又不会压住新出现的工步行。 */
     private static final long CAP_STEP_CACHE_MS = 5000L;
     private static final int SAMPLE_BATCH_LIMIT = 500;
+    /**
+     * 每条语句的查询截止时间。厂商库的服务端**没有**查询超时,客户端不设就是无限;
+     * JDBC URL 里的 {@code socketTimeout} 只管单次读,慢速滴流的查询仍可能长期占住连接。
+     */
+    private static final int QUERY_TIMEOUT_SECONDS = 10;
     private static final Pattern SAFE_TABLE_NAME = Pattern.compile("[A-Za-z0-9_]{1,64}");
     /** 设备时间按适配器时区解释;来源列是秒精度 datetime,小数部分按需接受。 */
     private static final DateTimeFormatter DEVICE_TIME = new DateTimeFormatterBuilder()
@@ -618,6 +623,8 @@
             Connection connection = dataSource.getConnection();
             try (PreparedStatement statement = connection.prepareStatement(sql))
             {
+                // 读取超时有两层:连接串的 socketTimeout 管单次读,这里给整条语句一个截止时间。
+                statement.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
                 for (int index = 0; index < parameters.length; index++)
                     statement.setObject(index + 1, parameters[index]);
                 try (ResultSet rows = statement.executeQuery())
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java b/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
index f682c0d..ea088fe 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
@@ -33,6 +33,12 @@
 public final class ReadOnlyVendorDataSource implements DataSource
 {
     private static final List<String> ALLOWED_PREFIXES = List.of("select", "show", "describe", "desc");
+    /**
+     * 加锁读会真的阻塞厂商写入,所以它虽然以 {@code select} 开头也要单独拒绝:
+     * 现场实测厂商库 {@code tx_isolation=REPEATABLE-READ},行锁会挡住上位机的写库。
+     */
+    private static final List<String> LOCKING_CLAUSES = List.of("for update", "for share",
+            "lock in share mode", "updlock", "holdlock", "xlock", "tablock");
 
     private final DataSource delegate;
     private final String sourceLabel;
@@ -117,13 +123,17 @@
 
     private void requireQuery(String sql)
     {
-        if (isQuery(sql))
+        boolean query = isQuery(sql);
+        boolean locking = query && containsLockingClause(sql);
+        if (query && !locking)
         {
             audit.recordForwarded(sql);
             return;
         }
         audit.recordRejected(sql);
-        throw new NonQueryStatementException("厂商数据源只允许查询语句:" + abbreviate(sql));
+        throw new NonQueryStatementException(locking
+                ? "厂商数据源禁止加锁读(会阻塞厂商写入):" + abbreviate(sql)
+                : "厂商数据源只允许查询语句:" + abbreviate(sql));
     }
 
     /** 只按语句首关键字判定,避免用正则近似 SQL 语法。 */
@@ -144,6 +154,21 @@
         return false;
     }
 
+    /**
+     * 语句里是否带加锁读子句。这是**语法层的拦截**,不是 SQL 解析器:厂商取数的语句由本模块固定给出,
+     * 不会把加锁子句藏在字符串常量里;新增取数语句时由 {@code ReadOnlyVendorDataSourceTest} 一起覆盖。
+     */
+    static boolean containsLockingClause(String sql)
+    {
+        if (sql == null) return false;
+        String normalized = stripLeadingComments(sql).toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
+        for (String clause : LOCKING_CLAUSES)
+        {
+            if (normalized.contains(clause)) return true;
+        }
+        return false;
+    }
+
     private static String stripLeadingComments(String sql)
     {
         String remaining = sql.trim();
diff --git a/battery-device-neware/src/test/java/com/battery/device/neware/ReadOnlyVendorDataSourceTest.java b/battery-device-neware/src/test/java/com/battery/device/neware/ReadOnlyVendorDataSourceTest.java
index 9141429..b78ac57 100644
--- a/battery-device-neware/src/test/java/com/battery/device/neware/ReadOnlyVendorDataSourceTest.java
+++ b/battery-device-neware/src/test/java/com/battery/device/neware/ReadOnlyVendorDataSourceTest.java
@@ -158,4 +158,37 @@
         assertTrue(database.statements().isEmpty());
         assertEquals(3, dataSource.audit().rejectedStatements().size());
     }
+
+    /** 加锁读以 select 开头却是真的会阻塞厂商写入,所以必须在到达驱动之前被单独拒绝。 */
+    @Test
+    void lockingReadsAreRejectedBeforeTheyReachTheDriver()
+    {
+        FakeVendorDatabase database = new FakeVendorDatabase();
+        ReadOnlyVendorDataSource dataSource = dataSourceOver(database);
+        for (String sql : List.of("select dev_uid from channel for update",
+                "select dev_uid from channel FOR SHARE",
+                "select dev_uid from channel lock in share mode",
+                "select dev_uid from channel with (updlock)",
+                "/* probe */ select dev_uid from channel with (tablock, holdlock)"))
+        {
+            assertTrue(ReadOnlyVendorDataSource.isQuery(sql), "加锁读仍然以查询开头:" + sql);
+            assertTrue(ReadOnlyVendorDataSource.containsLockingClause(sql), "应当识别为加锁读:" + sql);
+            assertThrows(NonQueryStatementException.class,
+                    () -> dataSource.getConnection().prepareStatement(sql), sql);
+        }
+        assertTrue(database.statements().isEmpty(), "加锁读不得到达驱动");
+        assertTrue(!ReadOnlyVendorDataSource.containsLockingClause("select dev_uid from channel"));
+        assertTrue(!ReadOnlyVendorDataSource.containsLockingClause(""));
+    }
+
+    /** 连接超时与读取超时都必须由我们显式给出,且不得写入只读事务参数(5.5 不支持)。 */
+    @Test
+    void vendorJdbcUrlCarriesBothTimeoutsAndNoReadOnlySessionVariable()
+    {
+        String url = NewareMysqlAdapterFactory.jdbcUrl("26.1.2.3", 3306, "bts63");
+
+        assertTrue(url.contains("connectTimeout="), url);
+        assertTrue(url.contains("socketTimeout="), url);
+        assertTrue(!url.contains("transaction_read_only"), url);
+    }
 }
diff --git a/battery-device-simulator/pom.xml b/battery-device-simulator/pom.xml
deleted file mode 100644
index 0c7b786..0000000
--- a/battery-device-simulator/pom.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent><groupId>com.ruoyi</groupId><artifactId>ruoyi</artifactId><version>3.9.2</version></parent>
-    <groupId>com.battery</groupId><artifactId>battery-device-simulator</artifactId>
-    <description>开发设备模拟器实现</description>
-    <dependencies>
-        <dependency><groupId>com.battery</groupId><artifactId>battery-device-api</artifactId></dependency>
-        <!-- 仅测试复用协议客户端验证模拟器边界,生产依赖方向仍保持单向。 -->
-        <dependency><groupId>com.battery</groupId><artifactId>battery-device-bts</artifactId><scope>test</scope></dependency>
-        <dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><scope>test</scope></dependency>
-        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
-    </dependencies>
-</project>
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
deleted file mode 100644
index 1dea589..0000000
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
+++ /dev/null
@@ -1,933 +0,0 @@
-package com.battery.device.simulator;
-
-import java.io.IOException;
-import java.io.StringReader;
-import java.math.BigDecimal;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamConstants;
-import javax.xml.stream.XMLStreamReader;
-import com.battery.device.api.DeviceAdapter;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.device.api.model.DeviceConnectionStatus;
-import com.battery.device.api.model.DeviceIdentity;
-import com.battery.device.api.model.DeviceObservation;
-import com.battery.device.api.model.DetectedChannel;
-import com.battery.device.api.model.DetectedDeviceMetadata;
-import com.battery.device.api.model.ChannelIdentity;
-import com.battery.device.api.model.AdapterCapabilities;
-import com.battery.device.api.model.RealtimeSnapshot;
-import com.battery.device.api.DeviceObservationAdapter;
-import com.battery.device.api.TestDataObservationAdapter;
-import com.battery.device.simulator.BtsSimulatorStateStore;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
-import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
-import com.battery.device.simulator.BtsSimulatorStateStore.RecentRequest;
-import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
-import com.battery.device.simulator.InMemoryBtsSimulatorStateStore;
-import com.battery.device.simulator.DevelopmentCollectorSimulation;
-import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
-
-/**
- * 提供由开发后端托管的临时只读 BTS 模拟端点。
- * 同时实现发现与测试数据观测契约:模拟器直接暴露自身状态,因此发现与取数按同一实例解析,
- * 不再需要单独注入一个扩展观测 Bean。TCP 端点仍保留给协议层测试使用。
- */
-public final class BtsDevelopmentSimulator implements DeviceAdapter, TestDataObservationAdapter
-{
-    public static final String HOST = "127.0.0.1";
-    public static final int DEFAULT_PORT = 15020;
-    public static final String ADAPTER_CODE = "dev-sim";
-    public static final String ADAPTER_NAME = "dev-sim BTS 模拟器";
-    public static final int POLL_INTERVAL_MS = 5000;
-    public static final String NEWARE_CT_4008T_5V6A_S1_F = "CT-4008T-5V6A-S1-F";
-    public static final String NEWARE_CT_4008TN_5V12A_S1_F = "CT-4008Tn-5V12A-S1-F";
-
-    private static final int MAX_CHANNEL_COUNT = 99;
-    private static final int MAX_RECENT_REQUESTS = 20;
-    private static final int TOMBSTONE_DISCOVERY_COUNT = 3;
-
-    private final ScriptedBtsTcpSimulator simulator;
-    private final BtsSimulatorStateStore stateStore;
-    private SimulatorState state;
-    private boolean running = true;
-    private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
-    private static final DateTimeFormatter DEVICE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
-
-    /** 返回固定的适配器实例代码,用于把模拟观测归属到 dev-sim 这一实例。 */
-    @Override
-    public String adapterCode()
-    {
-        return ADAPTER_CODE;
-    }
-
-    /** 返回只观测能力:支持发现与实时观测,不支持历史下载,也不支持向设备下发控制命令。 */
-    @Override
-    public AdapterCapabilities capabilities()
-    {
-        return AdapterCapabilities.observeOnly();
-    }
-
-    /** 直接提供模拟器当前拓扑,供不需要 TCP/XML 的契约测试使用。 */
-    @Override
-    public synchronized List<DetectedChannel> discoverChannels()
-    {
-        List<DetectedChannel> result = new ArrayList<>();
-        for (DeviceState device : state.devices())
-        {
-            BtsDeviceProfile profile = profileFor(device);
-            for (ChannelState channel : device.channels())
-            {
-                ChannelIdentity identity = new ChannelIdentity(ADAPTER_CODE, profile.devtype(), device.deviceId(),
-                        profile.subDeviceId(), channel.channelId());
-                result.add(new DetectedChannel(identity, device.ip(), state.softwareAccessible() && device.online() && channel.online(),
-                        new DetectedDeviceMetadata(profile.name(), profile.manufacturer(), profile.model(),
-                                simulatorMac(profile.devtype(), device.deviceId()))));
-            }
-        }
-        recordRequest("discover", ADAPTER_CODE, result.toString());
-        return List.copyOf(result);
-    }
-
-    /** 直接提供模拟器观测;离线通道不保留上一次测量或工步事实。 */
-    @Override
-    public synchronized java.util.Map<ChannelIdentity, DeviceObservation> observe(
-            java.util.Set<ChannelIdentity> channels)
-    {
-        if (channels == null || channels.isEmpty()) return java.util.Map.of();
-        java.util.Map<ChannelIdentity, DeviceObservation> result = new java.util.LinkedHashMap<>();
-        for (ChannelIdentity requested : channels) result.put(requested, observation(requested));
-        recordRequest("observe", channels.toString(), result.toString());
-        return java.util.Map.copyOf(result);
-    }
-
-    /** 直接提供模拟器实时快照;不可观测通道不出现在结果里,与 BTS 协议解析后的语义一致。 */
-    @Override
-    public synchronized Map<ChannelIdentity, RealtimeSnapshot> inquire(Set<ChannelIdentity> channels)
-    {
-        if (channels == null || channels.isEmpty()) return Map.of();
-        Map<ChannelIdentity, RealtimeSnapshot> result = new LinkedHashMap<>();
-        for (ChannelIdentity requested : channels)
-        {
-            if (!canObserve(requested)) continue;
-            CabinetRealtime measurement = cabinetMeasurement(requested);
-            if (measurement == null) continue;
-            DeviceObservation observed = observation(requested);
-            result.put(requested, new RealtimeSnapshot(requested, observed.workStatus(), measurement.cycleNo(),
-                    measurement.stepNo(), measurement.stepType(), null, BigDecimal.valueOf(measurement.currentA()),
-                    BigDecimal.valueOf(measurement.voltageV()), BigDecimal.valueOf(measurement.capacityAh()),
-                    BigDecimal.valueOf(measurement.energyWh()), totalTimeSeconds(requested),
-                    measurement.relativeTimeSeconds(), null, measurement.serverReceivedAt(), Map.of(),
-                    DeviceConnectionStatus.ONLINE, measurement.deviceTime(), List.of(), null));
-        }
-        recordRequest("inquire", channels.toString(), result.toString());
-        return Map.copyOf(result);
-    }
-
-    private long totalTimeSeconds(ChannelIdentity identity)
-    {
-        return requireDevice(identity.devid()).channels().get(identity.chlid() - 1).configuration().totalTimeSeconds();
-    }
-
-    private DeviceObservation observation(ChannelIdentity requested)
-    {
-        DeviceState device = state.devices().stream().filter(item -> item.deviceId() == requested.devid())
-                .findFirst().orElse(null);
-        ChannelState channel = device == null ? null : device.channels().stream()
-                .filter(item -> item.channelId() == requested.chlid()).findFirst().orElse(null);
-        if (!state.softwareAccessible() || device == null || channel == null
-                || !ADAPTER_CODE.equals(requested.adapterCode()) || !device.online() || !channel.online()
-                || requested.devtype() != profileFor(device).devtype()
-                || requested.subdevid() != profileFor(device).subDeviceId())
-            return DeviceObservation.offline(requested, failureReason());
-        return new DeviceObservation(new DeviceIdentity(ADAPTER_CODE, requested.devtype(), requested.devid(),
-                requested.subdevid()), requested, DeviceConnectionStatus.ONLINE, channel.workStatus(),
-                List.of(), null, java.util.Map.of(), null);
-    }
-
-    /** 启动仅监听 IPv4 本机回环地址的开发模拟器;传入 0 时由系统分配测试端口。 */
-    public BtsDevelopmentSimulator(int port) throws IOException
-    {
-        this(port, new InMemoryBtsSimulatorStateStore());
-    }
-
-    /** 从长期状态恢复后再启动 TCP 服务,避免服务先暴露默认拓扑。 */
-    public BtsDevelopmentSimulator(int port, BtsSimulatorStateStore stateStore) throws IOException
-    {
-        this.stateStore = stateStore;
-        state = stateStore.loadOrInitialize(defaultState());
-        validateRestoredState();
-        simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
-    }
-
-    /** 返回模拟器 TCP 端点实际监听的端口。 */
-    public int port()
-    {
-        return simulator.port();
-    }
-
-    /** 新增一台具有独立虚拟 IP 和通道数的开发设备。 */
-    public synchronized int addDevice(String ip, int channelCount)
-    {
-        validateDeviceConfiguration(ip, channelCount, null);
-        int deviceId = state.nextDeviceId();
-        List<DeviceState> devices = new ArrayList<>(state.devices());
-        devices.add(newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount)));
-        persist(new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, devices, state.collectors(),
-                deviceId + 1, state.nextCollectorId(), state.tombstones()));
-        return deviceId;
-    }
-
-    /** 修改开发设备的虚拟地址、通道数和在线状态。 */
-    public synchronized void updateDevice(int deviceId, String ip, int channelCount, boolean online)
-    {
-        DeviceState current = requireDevice(deviceId);
-        validateDeviceConfiguration(ip, channelCount, deviceId);
-        List<ChannelState> channels = new ArrayList<>(channelCount);
-        for (int channelId = 1; channelId <= channelCount; channelId++)
-        {
-            channels.add(channelId <= current.channels().size()
-                    ? current.channels().get(channelId - 1)
-                    : new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
-        }
-        List<DeviceState> devices = replaceDevice(new DeviceState(deviceId, ip.trim(), online, channels,
-                resizeProfile(current.profile(), channelCount)));
-        List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
-        for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++)
-            upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId);
-        tombstones.removeIf(item -> item.deviceId() == deviceId && item.channelId() <= channelCount);
-        persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
-                state.nextCollectorId(), tombstones));
-    }
-
-    /** 按预设新威型号元数据新增模拟设备;真实协议和拓扑仍待现场验证。 */
-    public synchronized int addNewareDevice(String ip, String model)
-    {
-        BtsDeviceProfile profile = newareProfile(model);
-        validateDeviceConfiguration(ip, profile.channelCount(), null);
-        int deviceId = state.nextDeviceId();
-        List<DeviceState> devices = new ArrayList<>(state.devices());
-        devices.add(newDevice(deviceId, ip, profile));
-        persist(new SimulatorState(state.version(), devices, state.collectors(), deviceId + 1,
-                state.nextCollectorId(), state.tombstones()));
-        return deviceId;
-    }
-
-    /** 将已有模拟设备切换为新威 profile,并按 profile 通道数调整拓扑。 */
-    public synchronized void configureNewareProfile(int deviceId, String model)
-    {
-        DeviceState current = requireDevice(deviceId);
-        BtsDeviceProfile profile = newareProfile(model);
-        updateDeviceWithProfile(current, profile);
-    }
-
-    /** 删除指定开发设备;后续发现响应不再返回它。 */
-    public synchronized void deleteDevice(int deviceId)
-    {
-        DeviceState removed = requireDevice(deviceId);
-        List<DeviceState> devices = new ArrayList<>(state.devices());
-        devices.removeIf(item -> item.deviceId() == deviceId);
-        List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
-        for (ChannelState channel : removed.channels())
-        {
-            upsertTombstone(tombstones, removed.deviceId(), removed.ip(), channel.channelId());
-        }
-        persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
-                state.nextCollectorId(), tombstones));
-    }
-
-    /** 修改指定开发设备的单个通道状态。 */
-    public synchronized void updateChannel(int deviceId, int channelId, boolean online,
-            DeviceWorkStatus workStatus)
-    {
-        requireControllableStatus(workStatus);
-        DeviceState device = requireDevice(deviceId);
-        if (channelId < 1 || channelId > device.channels().size())
-            throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size());
-        List<ChannelState> channels = new ArrayList<>(device.channels());
-        channels.set(channelId - 1, new ChannelState(channelId, online, workStatus,
-                channels.get(channelId - 1).configuration()));
-        persist(new SimulatorState(state.version(),
-                replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels, device.profile())), state.collectors(),
-                state.nextDeviceId(), state.nextCollectorId(), state.tombstones()));
-    }
-
-    /** 保存单通道的当前测量和工步方案,不改写在线或工作状态,不产生观测。 */
-    public synchronized void configureChannel(int deviceId, int channelId, ChannelConfiguration configuration)
-    {
-        if (configuration == null) throw new IllegalArgumentException("通道配置不能为空");
-        DeviceState device = requireDevice(deviceId);
-        if (channelId < 1 || channelId > device.channels().size())
-            throw new IllegalArgumentException("通道不存在");
-        List<ChannelState> channels = new ArrayList<>(device.channels());
-        ChannelState channel = channels.get(channelId - 1);
-        channels.set(channelId - 1, new ChannelState(channelId, channel.online(), channel.workStatus(), configuration));
-        persist(new SimulatorState(state.version(),
-                replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels, device.profile())),
-                state.collectors(), state.nextDeviceId(), state.nextCollectorId(), state.tombstones()));
-    }
-
-    /** 新增一台与 BTS 检测柜拓扑隔离的开发采集器。 */
-    public synchronized int addCollector(String ip, int channelCount)
-    {
-        validateCollectorConfiguration(ip, channelCount, null);
-        int collectorId = state.nextCollectorId();
-        List<CollectorState> collectors = new ArrayList<>(state.collectors());
-        collectors.add(newCollector(collectorId, ip, channelCount));
-        persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
-                collectorId + 1, state.tombstones()));
-        return collectorId;
-    }
-
-    /** 修改开发采集器的虚拟地址、通道数和在线状态。 */
-    public synchronized void updateCollector(int collectorId, String ip, int channelCount, boolean online)
-    {
-        validateCollectorConfiguration(ip, channelCount, collectorId);
-        CollectorState current = requireCollector(collectorId);
-        List<CollectorState> collectors = new ArrayList<>(state.collectors());
-        collectors.replaceAll(item -> item.collectorId() == collectorId
-                ? DevelopmentCollectorSimulation.resize(current, ip, channelCount, online, Instant.now())
-                : item);
-        persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
-                state.nextCollectorId(), state.tombstones()));
-    }
-
-    /** 删除指定开发采集器,不影响检测柜 TCP 发现结果。 */
-    public synchronized void deleteCollector(int collectorId)
-    {
-        CollectorState collector = requireCollector(collectorId);
-        List<CollectorState> collectors = new ArrayList<>(state.collectors());
-        collectors.removeIf(item -> item.collectorId() == collectorId);
-        SimulatorState next = new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
-                state.nextCollectorId(), state.tombstones());
-        persist(next);
-    }
-
-    /** 保存稳定 S 通道的固定测量值及自定义指标,下一次请求时形成观测。 */
-    public synchronized void configureCollectorChannel(int collectorId, String channelCode,
-            List<CollectorMetricState> metrics)
-    {
-        CollectorState current = requireCollector(collectorId);
-        replaceCollector(DevelopmentCollectorSimulation.configureChannel(current, channelCode, metrics));
-    }
-
-    /** 修改软件可访问性,不改变设备身份、在线配置或通道工作状态。 */
-    public synchronized void setSoftwareAccessible(boolean accessible)
-    {
-        SimulatorState next = new SimulatorState(state.version(), state.devices(), state.collectors(),
-                state.nextDeviceId(), state.nextCollectorId(), state.tombstones(), accessible, state.observationSequence());
-        stateStore.save(next);
-        state = next;
-    }
-
-    /** 批量修改设备在线事实,保留各通道工作状态。 */
-    public synchronized void setAllDevicesPoweredOn(boolean poweredOn)
-    {
-        List<DeviceState> devices = state.devices().stream()
-                .map(device -> new DeviceState(device.deviceId(), device.ip(), poweredOn, device.channels(), device.profile()))
-                .toList();
-        List<CollectorState> collectors = state.collectors().stream()
-                .map(collector -> DevelopmentCollectorSimulation.resize(collector, collector.ip(),
-                        collector.channelCount(), poweredOn, Instant.now())).toList();
-        persist(new SimulatorState(state.version(), devices, collectors, state.nextDeviceId(),
-                state.nextCollectorId(), state.tombstones()));
-    }
-
-    /** 修改采集器单个通道工作状态;不代表真实采集器控制能力。 */
-    public synchronized void updateCollectorChannelStatus(int collectorId, String channelCode, DeviceWorkStatus status)
-    {
-        requireControllableStatus(status);
-        replaceCollector(DevelopmentCollectorSimulation.configureStatus(requireCollector(collectorId), channelCode, status));
-    }
-
-    /** 返回通道已配置的开发方案及当前工步位置,不自动推进或生成历史。 */
-    @Override
-    public synchronized TestPlan testPlan(ChannelIdentity channel)
-    {
-        if (!canObserve(channel)) return null;
-        ChannelState source = requireDevice(channel.devid()).channels().get(channel.chlid() - 1);
-        ChannelConfiguration configuration = source.configuration();
-        List<PlanRow> rows = new ArrayList<>();
-        for (int index = 0; index < configuration.steps().size(); index++) {
-            var step = configuration.steps().get(index);
-            String status = index + 1 < configuration.stepNo() ? "已完成" : "待执行";
-            if (index + 1 == configuration.stepNo()) status = switch (source.workStatus()) {
-                case WORKING -> "进行中";
-                case PAUSED -> "暂停";
-                case FINISHED -> "已完成";
-                case PROTECTED -> "异常";
-                default -> "待执行";
-            };
-            // cyclePosition 表示循环内的工步位置,不能使用当前循环号,否则控制台会合并不同工步。
-            rows.add(new PlanRow("STEP", index + 1, index + 1, step.name(), step.durationSeconds(),
-                    step.voltageV(), status, null, null));
-        }
-        return new TestPlan(configuration.cycleNo(), configuration.stepNo(), configuration.plannedCycleCount(), rows);
-    }
-
-    /** 请求当前检测柜测量;离线或无法观测时不返回缓存值。 */
-    @Override
-    public synchronized CabinetRealtime latestCabinet(ChannelIdentity channel)
-    {
-        CabinetRealtime result = canObserve(channel) ? cabinetMeasurement(channel) : null;
-        recordRequest("cabinet-realtime", String.valueOf(channel),
-                result == null ? failureReason() : result.toString());
-        return result;
-    }
-
-    private CabinetRealtime cabinetMeasurement(ChannelIdentity identity)
-    {
-        DeviceState device = requireDevice(identity.devid());
-        ChannelState channel = device.channels().get(identity.chlid() - 1);
-        if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) return null;
-        Instant now = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
-        long sequence = nextObservationSequence();
-        ChannelConfiguration configuration = channel.configuration();
-        var step = configuration.steps().get(configuration.stepNo() - 1);
-        // 保存值直接成为当前观测;工作状态和设备量程不反向覆盖操作员配置。
-        CabinetRealtime result = new CabinetRealtime(sequence, now, configuration.currentA().doubleValue(),
-                configuration.voltageV().doubleValue(), configuration.capacityAh().doubleValue(), configuration.energyWh().doubleValue(),
-                configuration.cycleNo(), configuration.stepNo(), step.name(), step.stepType(),
-                now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId(), configuration.relativeTimeSeconds());
-        return result;
-    }
-
-    /** 请求采集器当前配置值;软件不可访问或设备离线时返回空测量。 */
-    @Override
-    public synchronized CollectorRealtime latestCollector(String adapterCode, String collectorCode)
-    {
-        CollectorState collector = state.collectors().stream().filter(item -> item.deviceCode().equals(collectorCode))
-                .findFirst().orElse(null);
-        if (collector == null) return null;
-        var snapshot = collectorRealtime(collector.collectorId());
-        return new CollectorRealtime(snapshot.collectorCode(), snapshot.online(), snapshot.sampleIntervalMs(),
-                snapshot.measurements().stream().map(item -> new CollectorMeasurement(item.channelCode(), item.sequenceId(),
-                        item.deviceTime(), item.deviceTimeRaw(), item.serverReceivedAt(), item.metricCode(), item.value(),
-                        item.unit())).toList(), DEVICE_ZONE.getId());
-    }
-
-    /** 模拟器不提供历史工步统计。 */
-    @Override
-    public List<StepRecord> stepRecords(ChannelIdentity channel) { return List.of(); }
-
-    /** 模拟器不提供历史循环统计。 */
-    @Override
-    public List<CycleRecord> cycleRecords(ChannelIdentity channel) { return List.of(); }
-
-    /** 使用后端正常持久化周期,不提供开发倍速。 */
-    @Override
-    public long persistenceIntervalMillis(ChannelIdentity channel) { return 60000L; }
-
-    private boolean canObserve(ChannelIdentity channel)
-    {
-        // 完整来源身份限定数据归属;不可访问和离线只改变观测结果。
-        return channel != null && observation(channel).connectionStatus() == DeviceConnectionStatus.ONLINE
-                && observation(channel).workStatus() != DeviceWorkStatus.UNOBSERVABLE;
-    }
-
-    /** 请求采集器当前观测,读取状态页面本身不会触发此请求。 */
-    public synchronized CollectorRealtimeSnapshot collectorRealtime(int collectorId)
-    {
-        CollectorState collector = requireCollector(collectorId);
-        boolean available = state.softwareAccessible() && collector.online();
-        if (available)
-        {
-            collector = DevelopmentCollectorSimulation.sample(collector, Instant.now(), nextObservationSequence());
-            replaceCollector(collector);
-        }
-        var result = new CollectorRealtimeSnapshot(collector.deviceCode(), available, collector.sampleIntervalMs(),
-                available ? collector.latestMeasurements() : List.of());
-        recordRequest("collector-realtime", collector.deviceCode(), available ? result.toString() : failureReason());
-        return result;
-    }
-
-    private String failureReason()
-    {
-        return state.softwareAccessible() ? "UNOBSERVABLE" : "SOFTWARE_UNAVAILABLE";
-    }
-
-    private long nextObservationSequence()
-    {
-        long sequence = Math.max(state.observationSequence() + 1, Instant.now().toEpochMilli());
-        SimulatorState next = new SimulatorState(state.version(), state.devices(), state.collectors(),
-                state.nextDeviceId(), state.nextCollectorId(), state.tombstones(), state.softwareAccessible(), sequence);
-        stateStore.save(next);
-        state = next;
-        return sequence;
-    }
-
-    /** 返回模拟器配置、当前状态和最近请求,不触发新的设备观测。 */
-    public synchronized StateSnapshot snapshot()
-    {
-        List<DeviceSnapshot> deviceSnapshots = state.devices().stream()
-                .map(device -> new DeviceSnapshot(device.deviceId(), device.ip(), device.online(), device.profile(),
-                        device.channels().stream()
-                                .map(channel -> new ChannelSnapshot(channel.channelId(), channel.online(),
-                                        channel.workStatus(), channel.configuration()))
-                                .toList()))
-                .toList();
-        List<CollectorSnapshot> collectorSnapshots = state.collectors().stream()
-                .map(collector -> new CollectorSnapshot(collector.collectorId(), collector.deviceCode(),
-                        collector.ip(), collector.mac(), collector.name(), collector.manufacturer(),
-                        collector.model(), collector.channelCount(), collector.online(), collector.sampleIntervalMs(),
-                        collector.channels(), state.softwareAccessible() && collector.online() ? collector.latestMeasurements() : List.of()))
-                .toList();
-        return new StateSnapshot(running, state.softwareAccessible(), HOST + ":" + port(), deviceSnapshots, collectorSnapshots,
-                stateStore.loadRecentRequests());
-    }
-
-    /** 人工恢复默认拓扑;这是正常运行中唯一清空长期模拟状态的入口。 */
-    public synchronized void reset()
-    {
-        SimulatorState defaultState = defaultState();
-        stateStore.reset(defaultState);
-        state = defaultState;
-    }
-
-    private SimulatorState defaultState()
-    {
-        return new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
-                List.of(newDevice(1, "127.0.0.2", 4)),
-                List.of(newCollector(1, "127.0.1.2", 4)), 2, 2, List.of());
-    }
-
-    private DeviceState newDevice(int deviceId, String ip, int channelCount)
-    {
-        return newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount));
-    }
-
-    private DeviceState newDevice(int deviceId, String ip, BtsDeviceProfile profile)
-    {
-        List<ChannelState> channels = new ArrayList<>(profile.channelCount());
-        for (int channelId = 1; channelId <= profile.channelCount(); channelId++)
-            channels.add(new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
-        return new DeviceState(deviceId, ip.trim(), true, channels, profile);
-    }
-
-    private CollectorState newCollector(int collectorId, String ip, int channelCount)
-    {
-        return new CollectorState(collectorId, "dev-sim-collector-" + collectorId,
-                ip.trim(), simulatorCollectorMac(collectorId), "dev-sim 采集器 " + collectorId,
-                "开发模拟器", "Virtual Collector", channelCount, true);
-    }
-
-    private void replaceCollector(CollectorState replacement)
-    {
-        List<CollectorState> collectors = new ArrayList<>(state.collectors());
-        collectors.replaceAll(item -> item.collectorId() == replacement.collectorId() ? replacement : item);
-        persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
-                state.nextCollectorId(), state.tombstones()));
-    }
-
-    private synchronized String respond(String request)
-    {
-        String command = readCommand(request);
-        String response = state.softwareAccessible() ? switch (command)
-        {
-            case "getdevinfo" -> deviceInfoResponse();
-            case "inquire" -> observationResponse(request);
-            default -> "<bts version=\"1.0\"><cmd>error</cmd></bts>";
-        } : "<bts version=\"1.0\"><cmd>error</cmd></bts>";
-        recordRequest(command, request, state.softwareAccessible() ? response : "SOFTWARE_UNAVAILABLE");
-        return response;
-    }
-
-    private void recordRequest(String command, String request, String response)
-    {
-        List<RecentRequest> recentRequests = new ArrayList<>(stateStore.loadRecentRequests());
-        recentRequests.add(0, new RecentRequest(Instant.now(), command, request, response));
-        if (recentRequests.size() > MAX_RECENT_REQUESTS)
-            recentRequests = new ArrayList<>(recentRequests.subList(0, MAX_RECENT_REQUESTS));
-        stateStore.saveRecentRequests(recentRequests);
-    }
-
-    private String readCommand(String request)
-    {
-        try
-        {
-            XMLInputFactory factory = XMLInputFactory.newFactory();
-            factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
-            factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
-            XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(request));
-            while (reader.hasNext())
-            {
-                if (reader.next() == XMLStreamConstants.START_ELEMENT && "cmd".equals(reader.getLocalName()))
-                    return reader.getElementText().trim();
-            }
-        }
-        catch (Exception exception)
-        {
-            throw new IllegalArgumentException("Invalid BTS request XML", exception);
-        }
-        throw new IllegalArgumentException("BTS request does not contain cmd");
-    }
-
-    private synchronized String deviceInfoResponse()
-    {
-        int channelCount = state.devices().stream().mapToInt(device -> device.channels().size()).sum()
-                + state.tombstones().size();
-        StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>getdevinfo_resp</cmd>")
-                .append("<serverip count=\"0\"/><middle count=\"").append(channelCount).append("\">");
-        for (DeviceState device : state.devices())
-        {
-            for (ChannelState channel : device.channels())
-            {
-                boolean available = device.online() && channel.online();
-                appendDiscoveredChannel(response, device.deviceId(), device.ip(), channel.channelId(), available);
-            }
-        }
-        List<ChannelTombstone> remaining = new ArrayList<>();
-        for (ChannelTombstone tombstone : state.tombstones())
-        {
-            appendDiscoveredChannel(response, tombstone.deviceId(), tombstone.ip(), tombstone.channelId(), false);
-            if (tombstone.remainingDiscoveries() > 1)
-                remaining.add(new ChannelTombstone(tombstone.deviceId(), tombstone.ip(), tombstone.channelId(),
-                        tombstone.remainingDiscoveries() - 1));
-        }
-        if (!remaining.equals(state.tombstones()))
-            persist(new SimulatorState(state.version(), state.devices(), state.collectors(), state.nextDeviceId(),
-                    state.nextCollectorId(), remaining));
-        return response.append("</middle></bts>").toString();
-    }
-
-    private void appendDiscoveredChannel(StringBuilder response, int deviceId, String ip, int channelId,
-            boolean available)
-    {
-        // 展示元数据是开发模拟器扩展,不代表真实 BTS 协议必然提供这些属性。
-        DeviceState device = state.devices().stream().filter(item -> item.deviceId() == deviceId).findFirst().orElse(null);
-        BtsDeviceProfile profile = device == null ? BtsDeviceProfile.virtual(channelId) : profileFor(device);
-        response.append("<channel ip=\"").append(ip).append("\" devtype=\"").append(profile.devtype())
-                .append("\" devid=\"").append(deviceId).append("\" subdevid=\"").append(profile.subDeviceId())
-                .append("\" Channelid=\"").append(channelId).append("\" name=\"").append(profile.name())
-                .append("\" manufacturer=\"").append(profile.manufacturer()).append("\" model=\"")
-                .append(profile.model()).append("\" serialNumber=\"").append(profile.serialNumber())
-                .append("\" firmwareVersion=\"").append(profile.firmwareVersion()).append("\" mac=\"")
-                .append(simulatorMac(profile.devtype(), deviceId)).append("\">")
-                .append(available).append("</channel>");
-    }
-
-    private synchronized String observationResponse(String request)
-    {
-        java.util.Set<String> requested = requestedChannels(request);
-        List<ObservedChannel> available = new ArrayList<>();
-        for (DeviceState device : state.devices())
-            for (ChannelState channel : device.channels())
-                if (device.online() && channel.online() && channel.workStatus() != DeviceWorkStatus.UNOBSERVABLE
-                        && requested.contains(profileFor(device).devtype() + ":" + device.deviceId()
-                                + ":" + profileFor(device).subDeviceId() + ":" + channel.channelId()))
-                    available.add(new ObservedChannel(device.deviceId(), channel));
-
-        StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>inquire_resp</cmd><list count=\"")
-                .append(available.size()).append("\">");
-        for (ObservedChannel observed : available)
-        {
-            ChannelState channel = observed.channel();
-            DeviceState device = requireDevice(observed.deviceId());
-            BtsDeviceProfile profile = profileFor(device);
-            CabinetRealtime measurement = cabinetMeasurement(new ChannelIdentity(ADAPTER_CODE, profile.devtype(),
-                    device.deviceId(), profile.subDeviceId(), channel.channelId()));
-            double current = measurement.currentA(), voltage = measurement.voltageV();
-            double capacity = measurement.capacityAh(), energy = measurement.energyWh();
-            int cycleNo = measurement.cycleNo(), stepNo = measurement.stepNo();
-            String stepType = measurement.stepType();
-            response.append("<inquire dev=\"").append(profile.devtype()).append('-').append(observed.deviceId())
-                    .append('-').append(profile.subDeviceId()).append('-').append(channel.channelId())
-                    .append("-0\" cycle_id=\"").append(cycleNo).append("\" step_id=\"").append(stepNo)
-                    .append("\" step_type=\"").append(stepType).append("\" workstatus=\"")
-                    .append(toBtsWorkStatus(channel.workStatus()))
-                    .append("\" current=\"").append(decimal(current)).append("\" voltage=\"")
-                    .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity))
-                    .append("\" energy=\"").append(decimal(energy)).append("\" totaltime=\"")
-                    .append(channel.configuration().totalTimeSeconds()).append("\" relativetime=\"")
-                    .append(channel.configuration().relativeTimeSeconds())
-                    .append("\" open_or_close=\"0\" log_code=\"0\">")
-                    // 模拟器返回当前设备方案,供统一观测模型展示;不代表真实 BTS 的完整方案字段。
-                    .append("<plan><step step_no=\"").append(stepNo).append("\" step_type=\"")
-                    .append(stepType).append("\" name=\"").append(stepType).append("\"/></plan></inquire>");
-        }
-        return response.append("</list></bts>").toString();
-    }
-
-    private java.util.Set<String> requestedChannels(String request)
-    {
-        java.util.Set<String> channels = new java.util.HashSet<>();
-        try
-        {
-            XMLInputFactory factory = XMLInputFactory.newFactory();
-            factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
-            factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
-            XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(request));
-            while (reader.hasNext())
-            {
-                if (reader.next() == XMLStreamConstants.START_ELEMENT && "inquire".equals(reader.getLocalName()))
-                {
-                    String devid = reader.getAttributeValue(null, "devid");
-                    String chlid = reader.getAttributeValue(null, "chlid");
-                    String devtype = reader.getAttributeValue(null, "devtype");
-                    String subdevid = reader.getAttributeValue(null, "subdevid");
-                    if (devtype != null && devid != null && subdevid != null && chlid != null)
-                        channels.add(devtype + ":" + devid + ":" + subdevid + ":" + chlid);
-                }
-            }
-            return channels;
-        }
-        catch (Exception exception)
-        {
-            throw new IllegalArgumentException("Invalid BTS inquire XML", exception);
-        }
-    }
-
-    private BtsDeviceProfile profileFor(DeviceState device)
-    {
-        return device.profile() == null ? BtsDeviceProfile.virtual(device.channels().size()) : device.profile();
-    }
-
-    private BtsDeviceProfile resizeProfile(BtsDeviceProfile profile, int channelCount)
-    {
-        return new BtsDeviceProfile(profile.devtype(), profile.subDeviceId(), profile.name(), profile.manufacturer(),
-                profile.model(), profile.serialNumber(), profile.firmwareVersion(), channelCount,
-                profile.currentMinA(), profile.currentMaxA(), profile.voltageMinV(), profile.voltageMaxV(),
-                profile.sampleIntervalMs());
-    }
-
-    private void updateDeviceWithProfile(DeviceState current, BtsDeviceProfile profile)
-    {
-        int channelCount = profile.channelCount();
-        List<ChannelState> channels = new ArrayList<>(channelCount);
-        for (int channelId = 1; channelId <= channelCount; channelId++)
-            channels.add(channelId <= current.channels().size() ? current.channels().get(channelId - 1)
-                    : new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
-        List<DeviceState> devices = replaceDevice(new DeviceState(current.deviceId(), current.ip(), current.online(),
-                channels, profile));
-        List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
-        for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++)
-            upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId);
-        tombstones.removeIf(item -> item.deviceId() == current.deviceId() && item.channelId() <= channelCount);
-        persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
-                state.nextCollectorId(), tombstones));
-    }
-
-    private BtsDeviceProfile newareProfile(String model)
-    {
-        if (NEWARE_CT_4008T_5V6A_S1_F.equals(model))
-            return new BtsDeviceProfile(22, 1, "新威 CT-4008T", "新威", model, "SIM-CT4008T-5V6A",
-                    "BTS7.5.4", 8, new BigDecimal("-6.000"), new BigDecimal("6.000"), BigDecimal.ZERO,
-                    new BigDecimal("5.000"), 100);
-        if (NEWARE_CT_4008TN_5V12A_S1_F.equals(model))
-            return new BtsDeviceProfile(22, 1, "新威 CT-4008Tn", "新威", model, "SIM-CT4008TN-5V12A",
-                    "BTS8.0", 8, new BigDecimal("-12.000"), new BigDecimal("12.000"), BigDecimal.ZERO,
-                    new BigDecimal("5.000"), 100);
-        throw new IllegalArgumentException("Unsupported Neware simulator model: " + model);
-    }
-
-    private String decimal(double value)
-    {
-        return java.math.BigDecimal.valueOf(value).toPlainString();
-    }
-
-    private String toBtsWorkStatus(DeviceWorkStatus status)
-    {
-        return switch (status)
-        {
-            case STOPPED -> "stop";
-            case WORKING -> "working";
-            case PAUSED -> "pause";
-            case FINISHED -> "finish";
-            case PROTECTED -> "protect";
-            default -> throw new IllegalArgumentException("Unsupported simulator work status: " + status);
-        };
-    }
-
-    private void validateDeviceConfiguration(String ip, int channelCount, Integer currentDeviceId)
-    {
-        if (!isIpv4(ip)) throw new IllegalArgumentException("Simulator device IP must be a valid IPv4 address");
-        if (channelCount < 1 || channelCount > MAX_CHANNEL_COUNT)
-            throw new IllegalArgumentException("channelCount must be between 1 and " + MAX_CHANNEL_COUNT);
-        boolean duplicate = state.devices().stream()
-                .anyMatch(device -> device.ip().equals(ip.trim())
-                        && device.deviceId() != (currentDeviceId == null ? -1 : currentDeviceId));
-        duplicate = duplicate || state.collectors().stream().anyMatch(collector -> collector.ip().equals(ip.trim()));
-        if (duplicate) throw new IllegalArgumentException("Simulator device IP must be unique");
-    }
-
-    private void validateCollectorConfiguration(String ip, int channelCount, Integer currentCollectorId)
-    {
-        if (!isIpv4(ip)) throw new IllegalArgumentException("Simulator collector IP must be a valid IPv4 address");
-        if (channelCount < 1 || channelCount > MAX_CHANNEL_COUNT)
-            throw new IllegalArgumentException("channelCount must be between 1 and " + MAX_CHANNEL_COUNT);
-        boolean duplicate = state.collectors().stream()
-                .anyMatch(collector -> collector.ip().equals(ip.trim())
-                        && collector.collectorId() != (currentCollectorId == null ? -1 : currentCollectorId));
-        duplicate = duplicate || state.devices().stream().anyMatch(device -> device.ip().equals(ip.trim()));
-        if (duplicate) throw new IllegalArgumentException("Simulator collector IP must be unique");
-    }
-
-    private boolean isIpv4(String ip)
-    {
-        if (ip == null) return false;
-        String[] parts = ip.trim().split("\\.", -1);
-        if (parts.length != 4) return false;
-        for (String part : parts)
-        {
-            try
-            {
-                int value = Integer.parseInt(part);
-                if (part.isEmpty() || value < 0 || value > 255) return false;
-            }
-            catch (NumberFormatException exception)
-            {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private DeviceState requireDevice(int deviceId)
-    {
-        return state.devices().stream().filter(item -> item.deviceId() == deviceId).findFirst()
-                .orElseThrow(() -> new IllegalArgumentException("Unknown simulator device: " + deviceId));
-    }
-
-    private CollectorState requireCollector(int collectorId)
-    {
-        return state.collectors().stream().filter(item -> item.collectorId() == collectorId).findFirst()
-                .orElseThrow(() -> new IllegalArgumentException("Unknown simulator collector: " + collectorId));
-    }
-
-    private List<DeviceState> replaceDevice(DeviceState replacement)
-    {
-        List<DeviceState> devices = new ArrayList<>(state.devices());
-        devices.replaceAll(item -> item.deviceId() == replacement.deviceId() ? replacement : item);
-        return devices;
-    }
-
-    private void upsertTombstone(List<ChannelTombstone> tombstones, int deviceId, String ip, int channelId)
-    {
-        tombstones.removeIf(item -> item.deviceId() == deviceId && item.channelId() == channelId);
-        tombstones.add(new ChannelTombstone(deviceId, ip, channelId, TOMBSTONE_DISCOVERY_COUNT));
-    }
-
-    private void persist(SimulatorState nextState)
-    {
-        // 写穿失败时保留当前内存副本,避免模拟器页面显示未持久化状态。
-        SimulatorState configured = new SimulatorState(nextState.version(), nextState.devices(), nextState.collectors(),
-                nextState.nextDeviceId(), nextState.nextCollectorId(), nextState.tombstones(),
-                state.softwareAccessible(), state.observationSequence());
-        stateStore.save(configured);
-        state = configured;
-    }
-
-    private String simulatorMac(int devtype, int devid)
-    {
-        return String.format("02:44:%02X:%02X:%02X:%02X",
-                (devtype >>> 8) & 0xFF, devtype & 0xFF, (devid >>> 8) & 0xFF, devid & 0xFF);
-    }
-
-    private String simulatorCollectorMac(int collectorId)
-    {
-        return String.format("02:43:00:00:%02X:%02X", (collectorId >>> 8) & 0xFF, collectorId & 0xFF);
-    }
-
-    private void requireControllableStatus(DeviceWorkStatus status)
-    {
-        if (status == null || (status != DeviceWorkStatus.STOPPED && status != DeviceWorkStatus.WORKING
-                && status != DeviceWorkStatus.PAUSED && status != DeviceWorkStatus.FINISHED
-                && status != DeviceWorkStatus.PROTECTED && status != DeviceWorkStatus.UNOBSERVABLE))
-            throw new IllegalArgumentException("Unsupported simulator work status");
-    }
-
-    private void validateRestoredState()
-    {
-        java.util.Set<Integer> deviceIds = new java.util.HashSet<>();
-        java.util.Set<Integer> collectorIds = new java.util.HashSet<>();
-        java.util.Set<String> ips = new java.util.HashSet<>();
-        int maximumDeviceId = 0;
-        for (DeviceState device : state.devices())
-        {
-            if (device.deviceId() < 1 || !deviceIds.add(device.deviceId()) || !isIpv4(device.ip())
-                    || !ips.add(device.ip()) || device.channels().isEmpty()
-                    || device.channels().size() > MAX_CHANNEL_COUNT)
-                throw new IllegalStateException("Invalid BTS simulator device state in Redis");
-            if (profileFor(device).channelCount() != device.channels().size())
-                throw new IllegalStateException("BTS simulator device profile channel count does not match topology");
-            maximumDeviceId = Math.max(maximumDeviceId, device.deviceId());
-            for (int index = 0; index < device.channels().size(); index++)
-            {
-                ChannelState channel = device.channels().get(index);
-                if (channel.channelId() != index + 1)
-                    throw new IllegalStateException("Invalid BTS simulator channel state in Redis");
-                requireControllableStatus(channel.workStatus());
-            }
-        }
-        int maximumCollectorId = 0;
-        for (CollectorState collector : state.collectors())
-        {
-            if (collector.collectorId() < 1 || !collectorIds.add(collector.collectorId())
-                    || !isIpv4(collector.ip()) || !ips.add(collector.ip())
-                    || collector.channelCount() < 1 || collector.channelCount() > MAX_CHANNEL_COUNT)
-                throw new IllegalStateException("Invalid BTS simulator collector state in Redis");
-            DevelopmentCollectorSimulation.validate(collector);
-            maximumCollectorId = Math.max(maximumCollectorId, collector.collectorId());
-        }
-        java.util.Set<String> tombstoneIds = new java.util.HashSet<>();
-        boolean invalidTombstone = state.tombstones().stream().anyMatch(item -> item.deviceId() < 1
-                || !isIpv4(item.ip()) || item.channelId() < 1 || item.channelId() > MAX_CHANNEL_COUNT
-                || item.remainingDiscoveries() < 1 || item.remainingDiscoveries() > TOMBSTONE_DISCOVERY_COUNT
-                || !tombstoneIds.add(item.deviceId() + ":" + item.channelId()));
-        if (state.nextDeviceId() <= maximumDeviceId || state.nextCollectorId() <= maximumCollectorId
-                || invalidTombstone)
-            throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis");
-    }
-
-    /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */
-    @Override
-    public synchronized void close()
-    {
-        running = false;
-        try
-        {
-            simulator.close();
-        }
-        catch (Exception exception)
-        {
-            throw new IllegalStateException("关闭开发模拟器失败", exception);
-        }
-    }
-
-    private record ObservedChannel(int deviceId, ChannelState channel) { }
-
-    public record StateSnapshot(boolean running, boolean softwareAccessible, String listenAddress, List<DeviceSnapshot> devices,
-            List<CollectorSnapshot> collectors, List<RecentRequest> recentRequests) { }
-
-    public record DeviceSnapshot(int deviceId, String ip, boolean online, BtsDeviceProfile profile,
-            List<ChannelSnapshot> channels) { }
-
-    public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus,
-            ChannelConfiguration configuration) { }
-
-    public record CollectorSnapshot(int collectorId, String deviceCode, String ip, String mac, String name,
-            String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs,
-            List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements)
-    {
-        public CollectorSnapshot
-        {
-            channels = List.copyOf(channels);
-            latestMeasurements = List.copyOf(latestMeasurements);
-        }
-    }
-
-}
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsSimulatorStateStore.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsSimulatorStateStore.java
deleted file mode 100644
index 9e7e01d..0000000
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsSimulatorStateStore.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package com.battery.device.simulator;
-
-import java.math.BigDecimal;
-import java.time.Instant;
-import java.util.List;
-import com.battery.device.api.model.DeviceWorkStatus;
-
-/** 持久化开发模拟器配置;实现必须在返回前确认写入成功。 */
-public interface BtsSimulatorStateStore
-{
-    int CURRENT_VERSION = 1;
-    /** 读取已有状态,仅在不存在时保存默认拓扑。 */
-    SimulatorState loadOrInitialize(SimulatorState defaultState);
-
-    /** 保存设备配置和观测序号;返回前必须完成写入。 */
-    void save(SimulatorState state);
-
-    /** 读取短期保留的最近请求。 */
-    List<RecentRequest> loadRecentRequests();
-
-    /** 保存短期保留的最近请求。 */
-    void saveRecentRequests(List<RecentRequest> requests);
-
-    /** 人工重置主状态和最近请求。 */
-    void reset(SimulatorState defaultState);
-
-    record SimulatorState(int version, List<DeviceState> devices, List<CollectorState> collectors,
-            int nextDeviceId, int nextCollectorId, List<ChannelTombstone> tombstones,
-            Boolean softwareAccessible, long observationSequence)
-    {
-        public SimulatorState(int version, List<DeviceState> devices, List<CollectorState> collectors,
-                int nextDeviceId, int nextCollectorId, List<ChannelTombstone> tombstones)
-        {
-            this(version, devices, collectors, nextDeviceId, nextCollectorId, tombstones, true, 0L);
-        }
-
-        public SimulatorState
-        {
-            if (version != CURRENT_VERSION)
-                throw new IllegalStateException("Unsupported BTS simulator state version: " + version);
-            softwareAccessible = softwareAccessible == null ? true : softwareAccessible;
-            devices = List.copyOf(devices);
-            collectors = List.copyOf(collectors);
-            tombstones = List.copyOf(tombstones);
-        }
-    }
-
-    record DeviceState(int deviceId, String ip, boolean online, List<ChannelState> channels,
-            BtsDeviceProfile profile)
-    {
-        public DeviceState
-        {
-            channels = List.copyOf(channels);
-            profile = profile == null ? BtsDeviceProfile.virtual(channels.size()) : profile;
-        }
-
-        /** 兼容旧版只保存拓扑和通道状态的模拟设备。 */
-        public DeviceState(int deviceId, String ip, boolean online, List<ChannelState> channels)
-        {
-            this(deviceId, ip, online, channels, BtsDeviceProfile.virtual(channels.size()));
-        }
-    }
-
-    record CollectorState(int collectorId, String deviceCode, String ip, String mac, String name,
-            String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs,
-            List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements)
-    {
-        public CollectorState
-        {
-            channels = List.copyOf(channels);
-            latestMeasurements = List.copyOf(latestMeasurements);
-        }
-
-        public CollectorState(int collectorId, String deviceCode, String ip, String mac, String name,
-                String manufacturer, String model, int channelCount, boolean online)
-        {
-            this(collectorId, deviceCode, ip, mac, name, manufacturer, model, channelCount, online,
-                    DevelopmentCollectorSimulation.DEFAULT_SAMPLE_INTERVAL_MS,
-                    DevelopmentCollectorSimulation.defaultChannels(channelCount), List.of());
-        }
-    }
-
-    record CollectorChannelState(String channelCode, List<CollectorMetricState> metrics, DeviceWorkStatus workStatus)
-    {
-        public CollectorChannelState
-        {
-            metrics = List.copyOf(metrics);
-            workStatus = workStatus == null ? DeviceWorkStatus.WORKING : workStatus;
-        }
-
-        public CollectorChannelState(String channelCode, List<CollectorMetricState> metrics)
-        {
-            this(channelCode, metrics, DeviceWorkStatus.WORKING);
-        }
-    }
-
-    record CollectorMetricState(String metricCode, BigDecimal value, String unit) { }
-
-    record CollectorRealtimeMeasurement(String adapterCode, String collectorCode, String channelCode,
-            long sequenceId, Instant deviceTime, String deviceTimeRaw, Instant serverReceivedAt,
-            String metricCode, BigDecimal value, String unit) { }
-
-    record ChannelState(int channelId, boolean online, DeviceWorkStatus workStatus,
-            ChannelConfiguration configuration)
-    {
-        public ChannelState
-        {
-            configuration = configuration == null ? ChannelConfiguration.defaults() : configuration;
-        }
-
-        public ChannelState(int channelId, boolean online, DeviceWorkStatus workStatus)
-        { this(channelId, online, workStatus, null); }
-    }
-
-    /** 通道当前观测配置与方案一起保存,避免当前工步引用不存在的方案行。 */
-    record ChannelConfiguration(BigDecimal currentA, BigDecimal voltageV, BigDecimal capacityAh,
-            BigDecimal energyWh, int cycleNo, int stepNo, int plannedCycleCount,
-            long totalTimeSeconds, long relativeTimeSeconds, List<SimulatorPlanStep> steps)
-    {
-        public ChannelConfiguration
-        {
-            for (BigDecimal value : new BigDecimal[] { currentA, voltageV, capacityAh, energyWh })
-                if (value == null || !Double.isFinite(value.doubleValue()))
-                    throw new IllegalArgumentException("通道测量值必须是有效数值");
-            if (steps == null || steps.isEmpty() || steps.stream().anyMatch(java.util.Objects::isNull))
-                throw new IllegalArgumentException("工步方案至少包含一个有效工步");
-            steps = List.copyOf(steps);
-            if (cycleNo < 1 || plannedCycleCount < cycleNo || stepNo < 1 || stepNo > steps.size()
-                    || totalTimeSeconds < 0 || relativeTimeSeconds < 0 || relativeTimeSeconds > totalTimeSeconds)
-                throw new IllegalArgumentException("循环、当前工步或时间配置无效");
-        }
-
-        /** 为新增通道和既有配置补齐固定观测值,不产生成功接收记录。 */
-        public static ChannelConfiguration defaults()
-        {
-            return new ChannelConfiguration(BigDecimal.ZERO, new BigDecimal("3.7"), new BigDecimal("1.2"),
-                    new BigDecimal("4.44"), 1, 1, 1, 0, 0,
-                    List.of(new SimulatorPlanStep("恒流充电", "cc", 60d, 3.7d)));
-        }
-    }
-
-    record SimulatorPlanStep(String name, String stepType, double durationSeconds, double voltageV)
-    {
-        public SimulatorPlanStep
-        {
-            if (name == null || name.isBlank() || stepType == null
-                    || !List.of("cc", "dc", "cv", "rest", "end", "cccv").contains(stepType)
-                    || !Double.isFinite(durationSeconds) || durationSeconds < 0 || !Double.isFinite(voltageV))
-                throw new IllegalArgumentException("工步名称、类型、时间或电压无效");
-            name = name.trim();
-        }
-    }
-
-    /** BTS 检测柜的可配置厂商 profile,用于设备发现元数据展示。 */
-    record BtsDeviceProfile(int devtype, int subDeviceId, String name, String manufacturer, String model,
-            String serialNumber, String firmwareVersion, int channelCount, BigDecimal currentMinA,
-            BigDecimal currentMaxA, BigDecimal voltageMinV, BigDecimal voltageMaxV, int sampleIntervalMs)
-    {
-        public BtsDeviceProfile
-        {
-            if (devtype < 0 || subDeviceId < 0 || channelCount < 1 || sampleIntervalMs < 1)
-                throw new IllegalArgumentException("Invalid BTS device profile limits");
-            if (name == null || name.isBlank() || manufacturer == null || manufacturer.isBlank()
-                    || model == null || model.isBlank() || serialNumber == null || serialNumber.isBlank()
-                    || firmwareVersion == null || firmwareVersion.isBlank())
-                throw new IllegalArgumentException("BTS device profile metadata must not be blank");
-            if (currentMinA == null || currentMaxA == null || voltageMinV == null || voltageMaxV == null
-                    || currentMinA.compareTo(currentMaxA) > 0 || voltageMinV.compareTo(voltageMaxV) > 0)
-                throw new IllegalArgumentException("Invalid BTS device profile ranges");
-        }
-
-        public static BtsDeviceProfile virtual(int channelCount)
-        {
-            return new BtsDeviceProfile(22, 1, "dev-sim 设备", "开发模拟器", "BTS Virtual Cabinet",
-                    "SIM-VIRTUAL", "dev-sim", channelCount, new BigDecimal("-6.000"),
-                    new BigDecimal("6.000"), BigDecimal.ZERO, new BigDecimal("5.000"), 100);
-        }
-    }
-
-    record ChannelTombstone(int deviceId, String ip, int channelId, int remainingDiscoveries) { }
-
-    record RecentRequest(Instant receivedAt, String command, String request, String response) { }
-}
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java
deleted file mode 100644
index b0975f8..0000000
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.battery.device.simulator;
-
-import java.math.BigDecimal;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.device.simulator.BtsSimulatorStateStore.*;
-
-/** 提供请求时的采集器当前值;S 通道仍是待现场验证的开发合同。 */
-public final class DevelopmentCollectorSimulation
-{
-    public static final int DEFAULT_SAMPLE_INTERVAL_MS = 1000;
-    private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
-    private static final DateTimeFormatter DEVICE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
-
-    private DevelopmentCollectorSimulation() { }
-
-    /** 创建带稳定 S 通道和三项固定演示指标的采集器状态。 */
-    public static List<CollectorChannelState> defaultChannels(int channelCount)
-    {
-        List<CollectorChannelState> channels = new ArrayList<>(channelCount);
-        for (int index = 1; index <= channelCount; index++)
-        {
-            channels.add(new CollectorChannelState(channelCode(index), List.of(
-                    new CollectorMetricState("temperature", new BigDecimal("25.0")
-                            .add(BigDecimal.valueOf(index, 1)), "℃"),
-                    new CollectorMetricState("positive_potential", new BigDecimal("3.700"), "V"),
-                    new CollectorMetricState("negative_potential", new BigDecimal("-0.100"), "V"))));
-        }
-        return List.copyOf(channels);
-    }
-
-
-    /** 调整设备拓扑,保留已有通道的配置。 */
-    public static CollectorState resize(CollectorState current, String ip, int channelCount, boolean online, Instant changedAt)
-    {
-        var defaults = defaultChannels(channelCount);
-        List<CollectorChannelState> channels = new ArrayList<>();
-        for (int index = 0; index < channelCount; index++)
-            channels.add(index < current.channels().size() ? current.channels().get(index) : defaults.get(index));
-        return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(),
-                current.name(), current.manufacturer(), current.model(), channelCount, online,
-                current.sampleIntervalMs(), channels, List.of());
-    }
-
-    /** 保存当前测量配置,等待下一次实时请求。 */
-    public static CollectorState configureChannel(CollectorState current, String channelCode, List<CollectorMetricState> metrics)
-    {
-        validateMetrics(metrics);
-        int index = parseChannelIndex(channelCode, current.channelCount());
-        List<CollectorChannelState> channels = new ArrayList<>(current.channels());
-        channels.set(index, new CollectorChannelState(channelCode, metrics, channels.get(index).workStatus()));
-        return copy(current, channels, List.of());
-    }
-
-    /** 保存通道工作状态,等待下一次实时请求。 */
-    public static CollectorState configureStatus(CollectorState current, String channelCode, DeviceWorkStatus status)
-    {
-        int index = parseChannelIndex(channelCode, current.channelCount());
-        List<CollectorChannelState> channels = new ArrayList<>(current.channels());
-        channels.set(index, new CollectorChannelState(channelCode, channels.get(index).metrics(), status));
-        return copy(current, channels, List.of());
-    }
-
-    /** 仅生成本次请求的当前值,不补算上一次请求与本次之间的空档。 */
-    public static CollectorState sample(CollectorState current, Instant now, long sequence)
-    {
-        Instant observedAt = now.truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
-        List<CollectorRealtimeMeasurement> measurements = new ArrayList<>();
-        for (CollectorChannelState channel : current.channels())
-        {
-            if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) continue;
-            for (CollectorMetricState metric : channel.metrics())
-                measurements.add(new CollectorRealtimeMeasurement("dev-sim", current.deviceCode(), channel.channelCode(),
-                        sequence, observedAt, DEVICE_TIME_FORMATTER.format(observedAt.atZone(DEVICE_ZONE)), observedAt,
-                        metric.metricCode(), metric.value(), metric.unit()));
-        }
-        return copy(current, current.channels(), measurements);
-    }
-
-    /** 校验恢复后的稳定通道和测量配置。 */
-    public static void validate(CollectorState state)
-    {
-        if (state.channels().size() != state.channelCount()) throw new IllegalStateException("Invalid collector topology");
-        for (int index = 0; index < state.channels().size(); index++)
-        {
-            var channel = state.channels().get(index);
-            if (!channelCode(index + 1).equals(channel.channelCode())) throw new IllegalStateException("Invalid collector channel identity");
-            validateMetrics(channel.metrics());
-        }
-    }
-
-    private static CollectorState copy(CollectorState current, List<CollectorChannelState> channels,
-            List<CollectorRealtimeMeasurement> measurements)
-    {
-        return new CollectorState(current.collectorId(), current.deviceCode(), current.ip(), current.mac(),
-                current.name(), current.manufacturer(), current.model(), current.channelCount(), current.online(),
-                current.sampleIntervalMs(), channels, measurements);
-    }
-
-    private static void validateMetrics(List<CollectorMetricState> metrics)
-    {
-        if (metrics == null || metrics.isEmpty())
-            throw new IllegalArgumentException("Collector channel must contain at least one metric");
-        java.util.HashSet<String> codes = new java.util.HashSet<>();
-        for (CollectorMetricState metric : metrics)
-        {
-            if (metric.metricCode() == null || metric.metricCode().isBlank() || metric.value() == null
-                    || !metric.metricCode().equals(metric.metricCode().trim())
-                    || metric.unit() == null || metric.unit().isBlank()
-                    || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode()))
-                throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique");
-        }
-    }
-
-    private static int parseChannelIndex(String value, int channelCount)
-    {
-        if (value == null || !value.matches("S-\\d{2}"))
-            throw new IllegalArgumentException("Collector channel must use S-01...S-N identity");
-        int index = Integer.parseInt(value.substring(2)) - 1;
-        if (index < 0 || index >= channelCount || !channelCode(index + 1).equals(value))
-            throw new IllegalArgumentException("Unknown collector channel: " + value);
-        return index;
-    }
-
-    private static String channelCode(int index)
-    {
-        return String.format("S-%02d", index);
-    }
-
-    public record CollectorRealtimeSnapshot(String collectorCode, boolean online, int sampleIntervalMs,
-            List<CollectorRealtimeMeasurement> measurements)
-    {
-        public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); }
-    }
-
-}
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java
deleted file mode 100644
index f7903f7..0000000
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.battery.device.simulator;
-
-import java.util.List;
-
-/** 测试和独立协议合同使用的内存状态存储。 */
-public final class InMemoryBtsSimulatorStateStore implements BtsSimulatorStateStore
-{
-    private SimulatorState state;
-    private List<RecentRequest> recentRequests = List.of();
-
-    public InMemoryBtsSimulatorStateStore() { }
-    public InMemoryBtsSimulatorStateStore(SimulatorState state) { this.state = state; }
-
-    /** 返回已有状态;尚未保存过任何状态时以 defaults 初始化并复用同一实例。 */
-    @Override
-    public synchronized SimulatorState loadOrInitialize(SimulatorState defaults)
-    {
-        if (state == null) state = defaults;
-        return state;
-    }
-
-    /** 保存状态引用,不做拷贝、校验或版本迁移。 */
-    @Override
-    public synchronized void save(SimulatorState state) { this.state = state; }
-    /** 返回最近请求列表;尚未保存过时为空列表。 */
-    @Override
-    public synchronized List<RecentRequest> loadRecentRequests() { return recentRequests; }
-    /** 保存最近请求的不可变副本,调用方后续修改原列表不影响这里。 */
-    @Override
-    public synchronized void saveRecentRequests(List<RecentRequest> requests) { recentRequests = List.copyOf(requests); }
-    /** 用 defaults 覆盖主状态并清空最近请求。 */
-    @Override
-    public synchronized void reset(SimulatorState defaults)
-    {
-        state = defaults;
-        recentRequests = List.of();
-    }
-}
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java
deleted file mode 100644
index 762ff2c..0000000
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java
+++ /dev/null
@@ -1,203 +0,0 @@
-package com.battery.device.simulator;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Function;
-
-/**
- * 提供可复用的 BTS TCP 脚本设备边界,用于闭环与恢复测试。
- */
-public final class ScriptedBtsTcpSimulator implements AutoCloseable
-{
-    private static final byte[] FRAME_END = "#\r\n".getBytes(StandardCharsets.UTF_8);
-
-    private final ServerSocket serverSocket;
-    private final int port;
-    /** 连接服务线程显式创建并命名。该池只提交一个常驻服务任务,队列容量 1 只是声明不再接受更多任务。 */
-    private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
-            new LinkedBlockingQueue<>(1), simulatorThreadFactory());
-    private final BlockingQueue<ScriptedAction> actions = new LinkedBlockingQueue<>();
-    private final List<String> requests = new CopyOnWriteArrayList<>();
-    private final Function<String, String> responseProvider;
-
-    public ScriptedBtsTcpSimulator() throws IOException
-    {
-        this(0, true, null);
-    }
-
-    private ScriptedBtsTcpSimulator(int port, boolean acceptConnections,
-            Function<String, String> responseProvider) throws IOException
-    {
-        if (port < 0 || port > 65535) throw new IllegalArgumentException("port must be between 0 and 65535");
-        this.responseProvider = responseProvider;
-        serverSocket = new ServerSocket();
-        serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), port));
-        this.port = serverSocket.getLocalPort();
-        if (acceptConnections)
-            executor.submit(this::serve);
-        else
-            serverSocket.close();
-    }
-
-    /** 创建一启动就关闭监听、任何连接都会被拒绝的模拟器。 */
-    public static ScriptedBtsTcpSimulator refusingConnections() throws IOException
-    {
-        return new ScriptedBtsTcpSimulator(0, false, null);
-    }
-
-    /** 创建仅监听本机、可按请求持续生成响应的模拟器。 */
-    public static ScriptedBtsTcpSimulator responding(int port, Function<String, String> responseProvider)
-            throws IOException
-    {
-        if (responseProvider == null) throw new IllegalArgumentException("responseProvider must not be null");
-        return new ScriptedBtsTcpSimulator(port, true, responseProvider);
-    }
-
-    /** 返回该模拟器绑定的本机端口;构造时传入 0 时返回的是系统分配端口而非 0。 */
-    public int port()
-    {
-        return port;
-    }
-
-    /** 向脚本队列追加一条零延迟应答,按 UTF-8 编码,由下一次请求按顺序取用。 */
-    public void enqueue(String response)
-    {
-        actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false));
-    }
-
-    /** 追加一条延迟应答:连接服务线程收到请求后先等待 delay 再写出;delay 不得为负。 */
-    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));
-    }
-
-    /** 追加一条零延迟的原始字节应答:字节原样写出后追加帧结束符,不做字符编码转换。 */
-    public void enqueueRaw(byte[] response)
-    {
-        actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false));
-    }
-
-    /** 追加一条断开动作:处理到该条时结束当前连接,且不写出任何应答。 */
-    public void enqueueDisconnect()
-    {
-        actions.add(new ScriptedAction(Duration.ZERO, null, true));
-    }
-
-    /** 返回已接收请求帧的只读快照;只有脚本模式(未配置 responseProvider)才记录。 */
-    public List<String> requests()
-    {
-        return List.copyOf(requests);
-    }
-
-    private void serve()
-    {
-        while (!serverSocket.isClosed())
-        {
-            try (Socket socket = serverSocket.accept())
-            {
-                serveConnection(socket);
-            }
-            catch (SocketException ex)
-            {
-                if (!serverSocket.isClosed()) throw new IllegalStateException(ex);
-            }
-            catch (Exception ignored)
-            {
-            }
-        }
-    }
-
-    private void serveConnection(Socket socket) throws Exception
-    {
-        while (!socket.isClosed())
-        {
-            String request = readFrame(socket.getInputStream());
-            if (responseProvider == null) requests.add(request);
-            ScriptedAction action = responseProvider == null
-                    ? actions.poll(5, TimeUnit.SECONDS)
-                    : new ScriptedAction(Duration.ZERO,
-                            responseProvider.apply(request).getBytes(StandardCharsets.UTF_8), false);
-            if (action == null) throw new IOException("No scripted BTS action available");
-            TimeUnit.MILLISECONDS.sleep(action.delay().toMillis());
-            if (action.disconnect()) return;
-            socket.getOutputStream().write(action.payload());
-            socket.getOutputStream().write(FRAME_END);
-            socket.getOutputStream().flush();
-        }
-    }
-
-    private String readFrame(InputStream input) throws IOException
-    {
-        return new String(readFrameBytes(input), StandardCharsets.UTF_8);
-    }
-
-    private byte[] readFrameBytes(InputStream input) throws IOException
-    {
-        java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
-        byte[] delimiter = FRAME_END;
-        int matched = 0;
-        while (true)
-        {
-            int current = input.read();
-            if (current < 0) throw new java.io.EOFException("BTS frame ended before delimiter");
-            if ((byte) current == delimiter[matched])
-            {
-                matched++;
-                if (matched == delimiter.length) return bytes.toByteArray();
-                continue;
-            }
-            if (matched > 0)
-            {
-                for (int i = 0; i < matched; i++) bytes.write(delimiter[i]);
-                matched = 0;
-                if ((byte) current == delimiter[0])
-                {
-                    matched = 1;
-                    continue;
-                }
-            }
-            bytes.write(current);
-        }
-    }
-
-    /** 关闭监听并终止连接服务线程,最多等待 2 秒,避免测试退出时残留线程。 */
-    @Override
-    public void close() throws Exception
-    {
-        serverSocket.close();
-        executor.shutdownNow();
-        executor.awaitTermination(2, TimeUnit.SECONDS);
-    }
-
-    /** 模拟器连接服务线程统一命名并设为守护线程,避免测试退出时残留。 */
-    private static ThreadFactory simulatorThreadFactory()
-    {
-        AtomicInteger sequence = new AtomicInteger();
-        return runnable -> {
-            Thread thread = new Thread(runnable, "bts-simulator-pool-" + sequence.incrementAndGet());
-            thread.setDaemon(true);
-            return thread;
-        };
-    }
-
-    private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect)
-    {
-    }
-}
diff --git a/battery-device-simulator/src/test/java/com/battery/device/contract/DiscoveryCandidateStoreContractTest.java b/battery-device-simulator/src/test/java/com/battery/device/contract/DiscoveryCandidateStoreContractTest.java
deleted file mode 100644
index 5a6af6a..0000000
--- a/battery-device-simulator/src/test/java/com/battery/device/contract/DiscoveryCandidateStoreContractTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.battery.device.contract;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import java.time.Instant;
-import java.util.List;
-import org.junit.jupiter.api.Test;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.device.api.model.ChannelIdentity;
-import com.battery.device.api.model.DeviceWorkStatus;
-
-class DiscoveryCandidateStoreContractTest
-{
-    @Test
-    void returnsOnlyLogicallyUnexpiredCandidates()
-    {
-        Instant now = Instant.parse("2026-08-27T00:00:00Z");
-        InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-        store.recordCabinets(List.of(candidate("expired", now.minusSeconds(1)),
-                candidate("current", now.plusSeconds(1))));
-
-        assertEquals(List.of("current"), store.snapshot(now).cabinets().stream()
-                .map(CabinetCandidate::deviceCode).toList());
-    }
-
-    @Test
-    void removesAllCandidateTypesForOneAdapter()
-    {
-        Instant now = Instant.parse("2026-08-27T00:00:00Z");
-        InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-        store.recordCabinets(List.of(candidate("dev-sim-cabinet", now.plusSeconds(30))));
-        store.replaceCollectors("dev-sim", List.of(new CollectorCandidate("dev-sim", "dev-sim-collector-1",
-                "127.0.1.2", null, "采集器", null, null, 4, true, now, now.plusSeconds(30))));
-
-        store.removeAdapter("dev-sim");
-
-        assertEquals(List.of(), store.snapshot(now).cabinets());
-        assertEquals(List.of(), store.snapshot(now).collectors());
-    }
-
-    private CabinetCandidate candidate(String code, Instant expiresAt)
-    {
-        Instant observedAt = Instant.parse("2026-08-26T23:59:55Z");
-        ChannelIdentity identity = new ChannelIdentity("dev-sim", 22, 1, 1, 1);
-        return new CabinetCandidate("dev-sim", null, code, 22, 1, "127.0.0.2", null,
-                code, null, null, List.of(new ChannelCandidate(identity, true,
-                        DeviceWorkStatus.STOPPED, observedAt)), observedAt, expiresAt);
-    }
-}
diff --git a/battery-device-simulator/src/test/java/com/battery/device/contract/SimulatorPublicContractTest.java b/battery-device-simulator/src/test/java/com/battery/device/contract/SimulatorPublicContractTest.java
deleted file mode 100644
index ad4b56d..0000000
--- a/battery-device-simulator/src/test/java/com/battery/device/contract/SimulatorPublicContractTest.java
+++ /dev/null
@@ -1,412 +0,0 @@
-package com.battery.device.contract;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.util.List;
-import java.util.Set;
-import org.junit.jupiter.api.Test;
-import com.alibaba.fastjson2.JSON;
-import com.battery.device.api.DeviceAdapter;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.bts.BtsDeviceAdapter;
-import com.battery.device.bts.BtsXmlCodec;
-import com.battery.device.bts.BtsTcpTransport;
-import com.battery.device.simulator.ScriptedBtsTcpSimulator;
-import com.battery.device.api.model.DetectedChannel;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.device.simulator.BtsSimulatorStateStore;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
-import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
-import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
-import com.battery.device.simulator.BtsSimulatorStateStore.RecentRequest;
-import com.battery.device.simulator.InMemoryBtsSimulatorStateStore;
-
-class SimulatorPublicContractTest
-{
-    @Test
-    void collectorRealtimeDropsMeasurementsRemovedFromCurrentConfiguration() throws Exception
-    {
-        Instant startedAt = Instant.parse("2026-08-29T04:00:00Z");
-        InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
-        {
-            simulator.collectorRealtime(1);
-            assertEquals(12, simulator.snapshot().collectors().get(0).latestMeasurements().size());
-
-            simulator.configureCollectorChannel(1, "S-01", List.of(
-                    new CollectorMetricState("temperature", new BigDecimal("27.0"), "℃")));
-            simulator.updateCollector(1, "127.0.1.2", 2, true);
-
-            simulator.collectorRealtime(1);
-            var snapshot = simulator.snapshot().collectors().get(0);
-            assertEquals(4, snapshot.latestMeasurements().size());
-            assertTrue(snapshot.latestMeasurements().stream()
-                    .noneMatch(item -> item.channelCode().equals("S-03")
-                            || item.channelCode().equals("S-04")
-                            || item.channelCode().equals("S-01")
-                                    && item.metricCode().equals("positive_potential")));
-        }
-
-        try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
-        {
-            assertEquals(4, restored.collectorRealtime(1).measurements().size());
-        }
-    }
-
-    @Test
-    void collectorRejectsMetricWhitespaceThatWouldBreakRestore() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
-                    List.of(new CollectorMetricState("temperature ", new BigDecimal("25.0"), "℃"))));
-            assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
-                    List.of(new CollectorMetricState("temperature", new BigDecimal("25.0"), " ℃"))));
-        }
-    }
-
-    @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());
-        }
-    }
-
-    @Test
-    void simulatorPreservesRequestEndingWithDelimiterPrefix() 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("ok");
-
-            assertEquals("ok", transport.exchange("payload#"));
-            assertEquals("payload#", simulator.requests().get(0));
-        }
-    }
-
-    @Test
-    void developmentSimulatorSupportsRepeatedDiscoveryAndObservation() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("development-bts", transport,
-                     new BtsXmlCodec("development-bts", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            var firstDiscovery = adapter.discoverChannels();
-            var secondDiscovery = adapter.discoverChannels();
-
-            assertEquals(4, firstDiscovery.size());
-            assertEquals(firstDiscovery, secondDiscovery);
-            assertEquals("开发模拟器", firstDiscovery.get(0).deviceMetadata().manufacturer());
-            assertEquals("BTS Virtual Cabinet", firstDiscovery.get(0).deviceMetadata().model());
-            assertEquals(1, firstDiscovery.stream().map(channel -> channel.identity().devid()).distinct().count());
-            assertEquals(4, firstDiscovery.stream().filter(channel -> channel.available()).count());
-
-            Set<com.battery.device.api.model.ChannelIdentity> channels = firstDiscovery.stream()
-                    .map(channel -> channel.identity())
-                    .collect(java.util.stream.Collectors.toSet());
-            var firstObservation = adapter.inquire(channels);
-            var secondObservation = adapter.inquire(channels);
-
-            assertEquals(4, firstObservation.size());
-            assertEquals(DeviceWorkStatus.STOPPED, firstObservation.values().iterator().next().workStatus());
-            assertEquals(firstObservation.keySet(), secondObservation.keySet());
-        }
-    }
-
-    @Test
-    void developmentSimulatorSupportsNewareProfilesAndFixedObservation() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            int tDevice = simulator.addNewareDevice("127.0.0.8", "CT-4008T-5V6A-S1-F");
-            int tnDevice = simulator.addNewareDevice("127.0.0.9", "CT-4008Tn-5V12A-S1-F");
-            simulator.updateChannel(tDevice, 1, true, DeviceWorkStatus.WORKING);
-
-            var discovered = adapter.discoverChannels();
-            var tChannels = discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.8")).toList();
-            var tnChannels = discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.9")).toList();
-
-            assertEquals(4, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.2")).count());
-            assertEquals(8, tChannels.size());
-            assertEquals(8, tnChannels.size());
-            assertEquals("新威", tChannels.get(0).deviceMetadata().manufacturer());
-            assertEquals("CT-4008T-5V6A-S1-F", tChannels.get(0).deviceMetadata().model());
-            assertEquals("CT-4008Tn-5V12A-S1-F", tnChannels.get(0).deviceMetadata().model());
-            assertEquals(8, simulator.snapshot().devices().stream()
-                    .filter(device -> device.deviceId() == tDevice || device.deviceId() == tnDevice)
-                    .mapToInt(device -> device.channels().size()).sum() / 2);
-
-            Set<com.battery.device.api.model.ChannelIdentity> channel = tChannels.stream()
-                    .map(DetectedChannel::identity).filter(identity -> identity.chlid() == 1)
-                    .collect(java.util.stream.Collectors.toSet());
-            var first = adapter.inquire(channel).values().iterator().next();
-            var second = adapter.inquire(channel).values().iterator().next();
-            assertEquals(DeviceWorkStatus.WORKING, first.workStatus());
-            assertEquals(first.currentA(), second.currentA());
-            assertTrue(first.voltageV().doubleValue() >= 0.0 && first.voltageV().doubleValue() <= 5.0);
-        }
-    }
-
-    @Test
-    void configuredChannelValuesReachBtsInquireWithoutClampingOrRounding() throws Exception
-    {
-        try (var simulator = new BtsDevelopmentSimulator(0);
-             var transport = new BtsTcpTransport("127.0.0.1", simulator.port(), Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong")))) {
-            simulator.configureChannel(1, 1, new com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration(
-                    new java.math.BigDecimal("-2.123456"), new java.math.BigDecimal("8.125"),
-                    new java.math.BigDecimal("7.2"), new java.math.BigDecimal("29.7"), 2, 2, 5, 120, 30,
-                    List.of(new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("充电", "cc", 60, 4.2),
-                            new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("放电 <验证>", "dc", 90, 3.0))));
-            var channel = adapter.discoverChannels().stream().filter(item -> item.identity().chlid() == 1)
-                    .findFirst().orElseThrow().identity();
-            var observation = adapter.inquire(Set.of(channel)).get(channel);
-            assertEquals(-2.123456, observation.currentA().doubleValue());
-            assertEquals(8.125, observation.voltageV().doubleValue());
-            assertEquals(7.2, observation.capacityAh().doubleValue());
-            assertEquals(29.7, observation.energyWh().doubleValue());
-            assertEquals(2, observation.cycleNo());
-            assertEquals(2, observation.stepNo());
-            assertEquals("dc", observation.stepType());
-            assertEquals(120L, observation.totalTimeSeconds());
-            assertEquals(30L, observation.relativeTimeSeconds());
-            assertEquals(30L, simulator.latestCabinet(channel).relativeTimeSeconds());
-        }
-    }
-
-    @Test
-    void developmentSimulatorPersistsNewareProfileMetadata() throws Exception
-    {
-        InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
-        {
-            simulator.addNewareDevice("127.0.0.8", "CT-4008Tn-5V12A-S1-F");
-        }
-
-        try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", restored.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            var profileChannels = adapter.discoverChannels().stream()
-                    .filter(channel -> channel.serverIp().equals("127.0.0.8")).toList();
-            assertEquals(8, profileChannels.size());
-            assertEquals("CT-4008Tn-5V12A-S1-F", profileChannels.get(0).deviceMetadata().model());
-            assertEquals("新威", profileChannels.get(0).deviceMetadata().manufacturer());
-        }
-    }
-
-    @Test
-    void developmentSimulatorPublishesOperatorControlledChannelFacts() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
-            simulator.updateChannel(1, 2, false, DeviceWorkStatus.PAUSED);
-
-            var discovered = adapter.discoverChannels();
-            Set<com.battery.device.api.model.ChannelIdentity> available = discovered.stream()
-                    .filter(channel -> channel.available())
-                    .map(channel -> channel.identity())
-                    .collect(java.util.stream.Collectors.toSet());
-            var observed = adapter.inquire(available);
-
-            assertEquals(4, discovered.size());
-            assertEquals(3, available.size());
-            assertEquals(DeviceWorkStatus.WORKING, observed.entrySet().stream()
-                    .filter(entry -> entry.getKey().chlid() == 1)
-                    .findFirst().orElseThrow().getValue().workStatus());
-
-            simulator.updateDevice(1, "127.0.0.2", 4, false);
-            assertEquals(0, adapter.discoverChannels().stream().filter(channel -> channel.available()).count());
-            assertEquals(4, simulator.snapshot().devices().get(0).channels().size());
-            assertEquals(3, simulator.snapshot().recentRequests().size());
-        }
-    }
-
-    @Test
-    void developmentSimulatorSupportsMultipleConfigurableDevices() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            int secondDeviceId = simulator.addDevice("127.0.0.3", 2);
-            simulator.updateDevice(1, "127.0.0.2", 4, true);
-
-            var discovered = adapter.discoverChannels();
-
-            assertEquals(6, discovered.size());
-            assertEquals(2, discovered.stream().map(channel -> channel.identity().devid()).distinct().count());
-            assertEquals(4, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.2")).count());
-            assertEquals(2, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.3")).count());
-
-            simulator.deleteDevice(secondDeviceId);
-            for (int index = 0; index < 3; index++)
-            {
-                var withTombstone = adapter.discoverChannels();
-                assertEquals(6, withTombstone.size());
-                assertEquals(4, withTombstone.stream().filter(DetectedChannel::available).count());
-            }
-            assertEquals(4, adapter.discoverChannels().size());
-        }
-    }
-
-    @Test
-    void developmentCollectorsNeverEnterBtsCabinetDiscovery() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            assertEquals(4, adapter.discoverChannels().size());
-
-            simulator.addCollector("127.0.1.8", 12);
-
-            assertEquals(4, adapter.discoverChannels().size());
-            assertEquals(2, simulator.snapshot().collectors().size());
-        }
-    }
-
-    @Test
-    void developmentSimulatorRestoresConfigurationNumberingAndStatus() throws Exception
-    {
-        InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
-        try (BtsDevelopmentSimulator first = new BtsDevelopmentSimulator(0, store))
-        {
-            first.updateDevice(1, "127.0.0.9", 2, false);
-            first.addDevice("127.0.0.10", 3);
-            first.updateCollector(1, "127.0.1.9", 7, false);
-        }
-
-        try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
-        {
-            assertEquals(2, restored.snapshot().devices().size());
-            assertEquals("127.0.0.9", restored.snapshot().devices().get(0).ip());
-            assertEquals(false, restored.snapshot().devices().get(0).online());
-            assertEquals(7, restored.snapshot().collectors().get(0).channelCount());
-            assertEquals(3, restored.addDevice("127.0.0.11", 1));
-        }
-    }
-
-    @Test
-    void developmentSimulatorDoesNotExposeWriteThroughFailureInMemory() throws Exception
-    {
-        InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
-        BtsSimulatorStateStore failing = new BtsSimulatorStateStore()
-        {
-            @Override public SimulatorState loadOrInitialize(SimulatorState value)
-            { return delegate.loadOrInitialize(value); }
-            @Override public void save(SimulatorState value) { throw new IllegalStateException("redis unavailable"); }
-            @Override public java.util.List<RecentRequest> loadRecentRequests()
-            { return delegate.loadRecentRequests(); }
-            @Override public void saveRecentRequests(java.util.List<RecentRequest> requests)
-            { delegate.saveRecentRequests(requests); }
-            @Override public void reset(SimulatorState value) { delegate.reset(value); }
-        };
-
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, failing))
-        {
-            assertThrows(IllegalStateException.class, () -> simulator.addDevice("127.0.0.3", 2));
-            assertEquals(1, simulator.snapshot().devices().size());
-        }
-    }
-
-    @Test
-    void collectorDeletionDoesNotReplaceMemoryWhenAtomicPersistenceFails() throws Exception
-    {
-        InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
-        BtsSimulatorStateStore failing = new BtsSimulatorStateStore()
-        {
-            @Override public SimulatorState loadOrInitialize(SimulatorState value)
-            { return delegate.loadOrInitialize(value); }
-            @Override public void save(SimulatorState value) { throw new IllegalStateException("redis unavailable"); }
-            @Override public java.util.List<RecentRequest> loadRecentRequests()
-            { return delegate.loadRecentRequests(); }
-            @Override public void saveRecentRequests(java.util.List<RecentRequest> requests)
-            { delegate.saveRecentRequests(requests); }
-            @Override public void reset(SimulatorState value) { delegate.reset(value); }
-        };
-
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, failing))
-        {
-            assertThrows(IllegalStateException.class, () -> simulator.deleteCollector(1));
-            assertEquals(1, simulator.snapshot().collectors().size());
-        }
-    }
-
-    @Test
-    void developmentSimulatorRestoresUnfinishedTombstones() throws Exception
-    {
-        InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
-        try (BtsDevelopmentSimulator first = new BtsDevelopmentSimulator(0, store);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", first.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            first.deleteDevice(1);
-            assertEquals(4, adapter.discoverChannels().size());
-        }
-
-        try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store);
-             BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", restored.port(),
-                     Duration.ofSeconds(1), Duration.ofSeconds(1));
-             DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
-                     new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
-        {
-            assertEquals(4, adapter.discoverChannels().size());
-            assertEquals(4, adapter.discoverChannels().size());
-            assertEquals(0, adapter.discoverChannels().size());
-        }
-    }
-
-    @Test
-    void developmentSimulatorRejectsNonContiguousRestoredChannelTopology()
-    {
-        SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
-                java.util.List.of(new DeviceState(1, "127.0.0.2", true,
-                        java.util.List.of(new ChannelState(2, true, DeviceWorkStatus.STOPPED)))),
-                java.util.List.of(), 2, 1, java.util.List.of());
-
-        assertThrows(IllegalStateException.class,
-                () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged)));
-    }
-
-}
diff --git a/battery-device-simulator/src/test/java/com/battery/device/simulator/SimulatorObservationAdapterContractTest.java b/battery-device-simulator/src/test/java/com/battery/device/simulator/SimulatorObservationAdapterContractTest.java
deleted file mode 100644
index a35fb53..0000000
--- a/battery-device-simulator/src/test/java/com/battery/device/simulator/SimulatorObservationAdapterContractTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package com.battery.device.simulator;
-
-import static org.junit.jupiter.api.Assertions.*;
-import org.junit.jupiter.api.Test;
-import com.battery.device.api.model.ChannelIdentity;
-import com.battery.device.api.model.DeviceConnectionStatus;
-import com.battery.device.api.model.DeviceWorkStatus;
-
-class SimulatorObservationAdapterContractTest
-{
-    @Test void configuredChannelFactsSurviveSoftwareFailureAndRestart() throws Exception
-    {
-        var store = new InMemoryBtsSimulatorStateStore();
-        long firstSequence;
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
-        {
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.PROTECTED);
-            simulator.updateCollectorChannelStatus(1, "S-01", DeviceWorkStatus.UNOBSERVABLE);
-            var collector = simulator.latestCollector(BtsDevelopmentSimulator.ADAPTER_CODE, "dev-sim-collector-1");
-            assertTrue(collector.measurements().stream().noneMatch(item -> item.channelCode().equals("S-01")));
-            firstSequence = collector.measurements().get(0).sequenceId();
-            simulator.setSoftwareAccessible(false);
-            simulator.updateDevice(1, "127.0.0.8", 4, false);
-            assertFalse(simulator.snapshot().softwareAccessible());
-        }
-        try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
-        {
-            assertFalse(restored.snapshot().softwareAccessible());
-            assertEquals("127.0.0.8", restored.snapshot().devices().get(0).ip());
-            assertEquals(DeviceWorkStatus.PROTECTED, restored.snapshot().devices().get(0).channels().get(0).workStatus());
-            assertTrue(restored.snapshot().collectors().get(0).latestMeasurements().isEmpty());
-            restored.setSoftwareAccessible(true);
-            assertTrue(restored.latestCollector(BtsDevelopmentSimulator.ADAPTER_CODE, "dev-sim-collector-1").measurements().get(0).sequenceId() > firstSequence);
-            assertEquals(DeviceWorkStatus.UNOBSERVABLE, restored.snapshot().collectors().get(0).channels().get(0).workStatus());
-        }
-    }
-
-    @Test void offlineCollectorDoesNotReturnLastSuccessAndStateReadsDoNotSample() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            assertTrue(simulator.snapshot().collectors().get(0).latestMeasurements().isEmpty());
-            var first = simulator.collectorRealtime(1);
-            var recent = simulator.snapshot().recentRequests();
-            assertEquals(recent, simulator.snapshot().recentRequests());
-            simulator.updateCollector(1, "127.0.1.2", 4, false);
-            assertFalse(simulator.collectorRealtime(1).online());
-            assertTrue(simulator.collectorRealtime(1).measurements().isEmpty());
-            simulator.updateCollector(1, "127.0.1.2", 4, true);
-            assertTrue(simulator.collectorRealtime(1).measurements().get(0).sequenceId() > first.measurements().get(0).sequenceId());
-            var channel = simulator.discoverChannels().get(0).identity();
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.UNOBSERVABLE);
-            assertNull(simulator.latestCabinet(channel));
-            assertEquals(DeviceWorkStatus.UNOBSERVABLE, simulator.observe(java.util.Set.of(channel)).get(channel).workStatus());
-        }
-    }
-
-    @Test void softwareFailurePreservesIdentityAndDoesNotReplayMeasurements() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            var channel = simulator.discoverChannels().get(0).identity();
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
-            var first = simulator.latestCabinet(channel);
-            assertNotNull(first);
-            assertFalse(simulator.latestCollector(BtsDevelopmentSimulator.ADAPTER_CODE, "dev-sim-collector-1").measurements().isEmpty());
-            simulator.setSoftwareAccessible(false);
-            assertEquals(channel, simulator.discoverChannels().get(0).identity());
-            assertTrue(simulator.snapshot().devices().get(0).online());
-            assertEquals(DeviceWorkStatus.UNOBSERVABLE,
-                    simulator.observe(java.util.Set.of(channel)).get(channel).workStatus());
-            assertNull(simulator.latestCabinet(channel));
-            assertTrue(simulator.latestCollector(BtsDevelopmentSimulator.ADAPTER_CODE, "dev-sim-collector-1").measurements().isEmpty());
-            simulator.setSoftwareAccessible(true);
-            var next = simulator.latestCabinet(channel);
-            assertTrue(next.sequenceId() > first.sequenceId());
-            assertFalse(next.deviceTime().isBefore(first.deviceTime()));
-            assertTrue(simulator.stepRecords(channel).isEmpty());
-            assertTrue(simulator.cycleRecords(channel).isEmpty());
-            assertEquals(60000L, simulator.persistenceIntervalMillis(channel));
-        }
-    }
-
-    @Test void persistenceRejectsForeignIdentitiesAndOfflineCachedFacts() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            ChannelIdentity channel = simulator.discoverChannels().get(0).identity();
-            simulator.updateChannel(channel.devid(), channel.chlid(), true, DeviceWorkStatus.WORKING);
-            assertNotNull(simulator.latestCabinet(channel));
-            for (ChannelIdentity foreign : java.util.List.of(
-                    new ChannelIdentity("another-adapter", channel.devtype(), channel.devid(), channel.subdevid(), channel.chlid()),
-                    new ChannelIdentity(channel.adapterCode(), 999, channel.devid(), channel.subdevid(), channel.chlid()),
-                    new ChannelIdentity(channel.adapterCode(), channel.devtype(), channel.devid(), 999, channel.chlid())))
-            {
-                assertNull(simulator.latestCabinet(foreign));
-                assertNull(simulator.testPlan(foreign));
-                assertTrue(simulator.stepRecords(foreign).isEmpty());
-                assertTrue(simulator.cycleRecords(foreign).isEmpty());
-            }
-            simulator.updateChannel(channel.devid(), channel.chlid(), false, DeviceWorkStatus.WORKING);
-            assertNull(simulator.latestCabinet(channel));
-            assertTrue(simulator.stepRecords(channel).isEmpty());
-            assertTrue(simulator.cycleRecords(channel).isEmpty());
-        }
-    }
-
-    @Test
-    void simulatorCanBeUsedDirectlyThroughTheVendorNeutralContract() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            ChannelIdentity channel = simulator.discoverChannels().get(0).identity();
-            simulator.updateChannel(channel.devid(), channel.chlid(), true, DeviceWorkStatus.WORKING);
-
-            var observation = simulator.observe(java.util.Set.of(channel)).get(channel);
-
-            assertEquals("dev-sim", simulator.adapterCode());
-            assertEquals(DeviceConnectionStatus.ONLINE, observation.connectionStatus());
-            assertEquals(DeviceWorkStatus.WORKING, observation.workStatus());
-        }
-    }
-}
diff --git a/pom.xml b/pom.xml
index 1913403..72c77b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -184,12 +184,6 @@
                 <artifactId>battery-device-neware</artifactId>
                 <version>${ruoyi.version}</version>
             </dependency>
-            <dependency>
-                <groupId>com.battery</groupId>
-                <artifactId>battery-device-simulator</artifactId>
-                <version>${ruoyi.version}</version>
-            </dependency>
-
             <!-- 通用工具-->
             <dependency>
                 <groupId>com.ruoyi</groupId>
@@ -213,14 +207,6 @@
         <module>ruoyi-common</module>
     </modules>
     <packaging>pom</packaging>
-    <profiles>
-        <profile>
-            <id>simulator</id>
-            <activation><activeByDefault>true</activeByDefault></activation>
-            <modules><module>battery-device-simulator</module></modules>
-        </profile>
-        <profile><id>without-simulator</id></profile>
-    </profiles>
 
     <build>
         <plugins>
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 6973c13..b47d636 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -117,39 +117,6 @@
         </dependency>
     </dependencies>
 
-    <profiles>
-        <profile>
-            <id>simulator</id>
-            <activation><activeByDefault>true</activeByDefault></activation>
-            <dependencies><dependency><groupId>com.battery</groupId><artifactId>battery-device-simulator</artifactId></dependency></dependencies>
-        </profile>
-        <profile>
-            <id>without-simulator</id>
-            <build><plugins><plugin>
-                <groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
-                <configuration>
-                    <excludes>
-                        <exclude>**/DevelopmentSimulatorConfig.java</exclude>
-                        <exclude>**/DevelopmentSimulatorDiscoveryScheduler.java</exclude>
-                        <exclude>**/DevelopmentCollectorSamplingScheduler.java</exclude>
-                        <exclude>**/DevelopmentCollectorStatusSynchronizer.java</exclude>
-                        <exclude>**/RedisBtsSimulatorStateStore.java</exclude>
-                        <exclude>**/BtsDevelopmentSimulatorController.java</exclude>
-                        <exclude>**/BtsDevelopmentSimulatorWebConfig.java</exclude>
-                    </excludes>
-                    <testExcludes>
-                        <testExclude>**/BtsDevelopmentSimulatorControllerTest.java</testExclude>
-                        <testExclude>**/DevelopmentSimulatorProfileContractTest.java</testExclude>
-                        <testExclude>**/DevelopmentCollectorStatusSynchronizerTest.java</testExclude>
-                        <testExclude>**/RedisStateStoreTest.java</testExclude>
-                        <testExclude>**/DeviceAdapterRuntimeConfigTest.java</testExclude>
-                        <testExclude>**/CollectorDiscoveryServiceImplTest.java</testExclude>
-                    </testExcludes>
-                </configuration>
-            </plugin></plugins></build>
-        </profile>
-    </profiles>
-
     <build>
         <plugins>
             <plugin>
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java
deleted file mode 100644
index 092b525..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.battery.business.runtime;
-
-import java.net.InetAddress;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.HandlerInterceptor;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-/** 将临时 BTS 模拟器页面和控制 API 限制在本机。 */
-@Configuration
-public class BtsDevelopmentSimulatorWebConfig implements WebMvcConfigurer
-{
-    /** 只为模拟器页面和控制 API 挂本机拦截器,其余路径不受影响。 */
-    @Override
-    public void addInterceptors(InterceptorRegistry registry)
-    {
-        registry.addInterceptor(new LocalOnlyInterceptor())
-                .addPathPatterns("/dev/bts-simulator.html", "/dev/bts-simulator/**");
-    }
-
-    public static final class LocalOnlyInterceptor implements HandlerInterceptor
-    {
-        @Override
-        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
-                throws Exception
-        {
-            if (isLoopback(request.getRemoteAddr())) return true;
-            response.sendError(HttpServletResponse.SC_FORBIDDEN, "BTS simulator controls are local-only");
-            return false;
-        }
-
-        private boolean isLoopback(String remoteAddress)
-        {
-            try
-            {
-                return remoteAddress != null && InetAddress.getByName(remoteAddress).isLoopbackAddress();
-            }
-            catch (Exception ignored)
-            {
-                return false;
-            }
-        }
-    }
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
deleted file mode 100644
index de8fe36..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.battery.business.runtime;
-
-import java.util.List;
-import java.util.Set;
-import java.time.Instant;
-import org.springframework.stereotype.Service;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
-import com.battery.business.domain.vo.DiscoveredCollectorVo;
-import com.battery.business.service.ICollectorDiscoveryService;
-import com.battery.business.service.ITCollectorService;
-
-/** 在应用装配边界将临时模拟采集器暴露给现有采集器页面。 */
-@Service
-public class DevelopmentCollectorDiscoveryService implements ICollectorDiscoveryService
-{
-    private final DeviceDiscoveryCandidateStore candidateStore;
-    private final ITCollectorService collectorService;
-
-    public DevelopmentCollectorDiscoveryService(DeviceDiscoveryCandidateStore candidateStore,
-            ITCollectorService collectorService)
-    {
-        this.candidateStore = candidateStore;
-        this.collectorService = collectorService;
-    }
-
-    /** 返回未过期且未登记的采集器,离线事实不影响发现展示或登记资格。 */
-    @Override
-    public List<DiscoveredCollectorVo> selectDiscoveredCollectorList()
-    {
-        Set<String> registeredCodes = collectorService.selectRegisteredCollectorCodes();
-        return candidateStore.snapshot(Instant.now()).collectors().stream()
-                .filter(collector -> !registeredCodes.contains(collector.deviceCode()))
-                .map(collector -> new DiscoveredCollectorVo(collector.deviceCode(), collector.ip(), collector.mac(),
-                        collector.name(), collector.manufacturer(), collector.model(), collector.channelCount(),
-                        collector.online() ? "在线" : "离线"))
-                .toList();
-    }
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizer.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizer.java
deleted file mode 100644
index e5de182..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizer.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.battery.business.runtime;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Profile;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
-import com.battery.business.mapper.TCollectorMapper;
-
-/** 将模拟采集器的发现写入 Redis,并同步已登记采集器的实时在线事实。 */
-@Component
-@Profile("dev-simulator")
-public class DevelopmentCollectorStatusSynchronizer
-{
-    private final BtsDevelopmentSimulator simulator;
-    private final DeviceDiscoveryCandidateStore candidateStore;
-    private final TCollectorMapper collectorMapper;
-
-    @Autowired
-    public DevelopmentCollectorStatusSynchronizer(BtsDevelopmentSimulator simulator,
-            DeviceDiscoveryCandidateStore candidateStore, TCollectorMapper collectorMapper)
-    {
-        this.simulator = simulator;
-        this.candidateStore = candidateStore;
-        this.collectorMapper = collectorMapper;
-    }
-
-    /** 保留单元测试及独立模拟器装配使用的旧构造入口。 */
-
-    /** 完整替换 dev-sim 采集器快照,删除配置后候选立即消失。 */
-    @Scheduled(fixedDelay = BtsDevelopmentSimulator.POLL_INTERVAL_MS)
-    public synchronized void synchronizeOnlineStatus()
-    {
-        Instant observedAt = Instant.now();
-        var simulatorState = simulator.snapshot();
-        List<CollectorCandidate> candidates = simulatorState.collectors().stream()
-                .map(item -> new CollectorCandidate(BtsDevelopmentSimulator.ADAPTER_CODE, item.deviceCode(),
-                        item.ip(), item.mac(), item.name(),
-                        item.manufacturer(), item.model(), item.channelCount(), simulatorState.softwareAccessible() && item.online(), observedAt,
-                        observedAt.plus(Duration.ofSeconds(30))))
-                .toList();
-        candidateStore.replaceCollectors(BtsDevelopmentSimulator.ADAPTER_CODE, candidates);
-        candidates.forEach(candidate -> collectorMapper.updateRegisteredObservation(candidate.deviceCode(),
-                candidate.online() ? "ONLINE" : "OFFLINE"));
-    }
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java
deleted file mode 100644
index d1aa1f0..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.battery.business.runtime;
-import java.io.IOException;
-import org.springframework.boot.ApplicationRunner;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.*;
-import com.battery.device.api.registry.*;
-import com.battery.device.api.model.AdapterCapabilities;
-import com.battery.device.simulator.*;
-/** 开发模拟器装配可整体移除,业务和真实 BTS 不引用此配置。 */
-@Configuration
-@Profile("dev-simulator")
-public class DevelopmentSimulatorConfig {
-    /** 按配置的监听端口装配开发模拟器;仅 dev-simulator 生效,容器关闭时释放其端口。 */
-    @Bean(destroyMethod = "close")
-    @Profile("dev-simulator")
-    public BtsDevelopmentSimulator btsDevelopmentSimulator(BtsSimulatorStateStore stateStore,
-            @Value("${battery.dev-simulator.port:15020}") int simulatorPort) throws IOException
-    {
-        return new BtsDevelopmentSimulator(simulatorPort, stateStore);
-    }
-
-    /**
-     * 将固定开发适配器按代码装配注册进运行时实例表,不写 t_device_adapter。
-     *
-     * <p>其运行时能力只有发现与实时观测,控制能力为不支持,不向设备发送控制命令。</p>
-     */
-    @Bean
-    @Profile("dev-simulator")
-    public ApplicationRunner registerDevelopmentSimulatorAdapter(DeviceAdapterRegistry registry,
-            BtsDevelopmentSimulator simulator,
-            @Value("${battery.dev-simulator.port:15020}") int simulatorPort)
-    {
-        // 固定开发装配不进入 t_device_adapter,直接注册运行时实例。
-        // 注册模拟器对象本身,使发现与取数落在同一实例上,不再单独注入扩展观测 Bean。
-        return arguments -> registry.register(developmentSimulatorRuntimeConfig(simulatorPort), simulator);
-    }
-
-    private AdapterInstanceConfig developmentSimulatorRuntimeConfig(int simulatorPort)
-    {
-        return new AdapterInstanceConfig(BtsDevelopmentSimulator.ADAPTER_CODE, "BTS", BtsDevelopmentSimulator.HOST,
-                simulatorPort, BtsDevelopmentSimulator.POLL_INTERVAL_MS,
-                "Asia/Hong_Kong", true,
-                new AdapterCapabilities(true, true, false, false));
-    }
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorDiscoveryScheduler.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorDiscoveryScheduler.java
deleted file mode 100644
index bbfe583..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorDiscoveryScheduler.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.battery.business.runtime;
-
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.context.annotation.Profile;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.api.registry.DeviceAdapterRegistry;
-import com.battery.business.service.impl.DeviceDiscoveryPersistenceService;
-import com.battery.business.service.impl.DeviceDiscoveryPersistenceService.DiscoverySource;
-
-/** 轮询代码固定装配的 dev-sim;该运行时不依赖数据库适配器记录。 */
-@Component
-@Profile("dev-simulator")
-public class DevelopmentSimulatorDiscoveryScheduler
-{
-    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DevelopmentSimulatorDiscoveryScheduler.class);
-    private final DeviceAdapterRegistry registry;
-    private final DeviceDiscoveryPersistenceService persistence;
-    public DevelopmentSimulatorDiscoveryScheduler(DeviceAdapterRegistry registry,
-            DeviceDiscoveryPersistenceService persistence)
-    {
-        this.registry = registry;
-        this.persistence = persistence;
-    }
-
-    /** 注册表已包含固定开发适配器时,将一轮检测柜观测写入 Redis 候选。 */
-    @Scheduled(fixedDelay = BtsDevelopmentSimulator.POLL_INTERVAL_MS)
-    public void discover()
-    {
-        if (!registry.contains(BtsDevelopmentSimulator.ADAPTER_CODE)) return;
-        try
-        {
-            persistence.discoverForReception(registry, new DiscoverySource(BtsDevelopmentSimulator.ADAPTER_CODE, null,
-                    BtsDevelopmentSimulator.ADAPTER_NAME, BtsDevelopmentSimulator.POLL_INTERVAL_MS));
-        }
-        catch (RuntimeException exception)
-        {
-            persistence.markUnavailable(BtsDevelopmentSimulator.ADAPTER_CODE);
-            log.warn("模拟设备观测失败,已清除过期实时值:{}", exception.getMessage());
-        }
-    }
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
deleted file mode 100644
index 61dba9d..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
+++ /dev/null
@@ -1,233 +0,0 @@
-package com.battery.business.runtime;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-import org.springframework.context.annotation.Profile;
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONArray;
-import com.alibaba.fastjson2.JSONObject;
-import com.battery.device.simulator.BtsSimulatorStateStore;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration;
-import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep;
-import com.battery.device.api.model.DeviceWorkStatus;
-
-/** 将开发模拟器完整状态写入 Redis,主状态不设置普通 TTL。 */
-@Component
-@Profile("dev-simulator")
-public class RedisBtsSimulatorStateStore implements BtsSimulatorStateStore
-{
-    static final String STATE_KEY = "dev:bts-simulator:state:v1";
-    static final String RECENT_REQUESTS_KEY = "dev:bts-simulator:recent-requests:v1";
-    private static final long RECENT_REQUESTS_TTL_HOURS = 1;
-
-    private final RedisTemplate<Object, Object> redisTemplate;
-
-    public RedisBtsSimulatorStateStore(RedisTemplate<Object, Object> redisTemplate)
-    {
-        this.redisTemplate = redisTemplate;
-    }
-
-    /** 读取模拟器主状态,不存在时原子写入默认拓扑;并发初始化时以 Redis 中先写入的那份为准。 */
-    @Override
-    public SimulatorState loadOrInitialize(SimulatorState defaultState)
-    {
-        Object stored = redisTemplate.opsForValue().get(STATE_KEY);
-        if (stored != null) return restoreState(stored);
-        String initial = JSON.toJSONString(defaultState);
-        if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(STATE_KEY, initial))) return defaultState;
-        stored = redisTemplate.opsForValue().get(STATE_KEY);
-        if (stored == null) throw new IllegalStateException("BTS simulator state initialization lost in Redis");
-        return restoreState(stored);
-    }
-
-    /** 整体覆盖模拟器主状态;该键不设普通 TTL。 */
-    @Override
-    public void save(SimulatorState state)
-    {
-        redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
-    }
-
-    /** 读取短期保留的最近请求;键不存在时返回空列表。 */
-    @Override
-    public List<RecentRequest> loadRecentRequests()
-    {
-        Object stored = redisTemplate.opsForValue().get(RECENT_REQUESTS_KEY);
-        if (stored == null) return List.of();
-        return List.copyOf(JSON.parseArray(stored.toString(), RecentRequest.class));
-    }
-
-    /** 覆盖保存最近请求,整键按小时级 TTL 自动过期。 */
-    @Override
-    public void saveRecentRequests(List<RecentRequest> requests)
-    {
-        redisTemplate.opsForValue().set(RECENT_REQUESTS_KEY, JSON.toJSONString(requests),
-                RECENT_REQUESTS_TTL_HOURS, TimeUnit.HOURS);
-    }
-
-    /** 人工重置:删除最近请求并写回默认主状态。 */
-    @Override
-    public void reset(SimulatorState defaultState)
-    {
-        redisTemplate.delete(RECENT_REQUESTS_KEY);
-        save(defaultState);
-    }
-
-    private SimulatorState restoreState(Object stored)
-    {
-        try
-        {
-            // 通用 RedisTemplate 在可执行包中可能将泛型数组恢复为 JSONObject,必须在存储边界逐层重建。
-            JSONObject root = jsonObject(stored);
-            Integer version = root.getInteger("version");
-            if (version == null || version != CURRENT_VERSION)
-                throw new IllegalStateException("Unsupported BTS simulator state version: " + version);
-            List<DeviceState> devices = new ArrayList<>();
-            requiredArray(root, "devices").forEach(item -> devices.add(deviceState(item)));
-            List<CollectorState> collectors = new ArrayList<>();
-            requiredArray(root, "collectors").forEach(item -> collectors.add(collectorState(jsonObject(item))));
-            List<ChannelTombstone> tombstones = new ArrayList<>();
-            requiredArray(root, "tombstones").forEach(item -> tombstones.add(channelTombstone(item)));
-            SimulatorState state = new SimulatorState(version, devices, collectors,
-                    requiredInteger(root, "nextDeviceId"), requiredInteger(root, "nextCollectorId"), tombstones,
-                    root.getBoolean("softwareAccessible"), root.getLongValue("observationSequence"));
-            return state;
-        }
-        catch (IllegalStateException exception)
-        {
-            throw exception;
-        }
-        catch (Exception exception)
-        {
-            throw new IllegalStateException("Invalid BTS simulator state in Redis", exception);
-        }
-    }
-
-    private JSONObject jsonObject(Object value)
-    {
-        if (value instanceof JSONObject object) return object;
-        if (value instanceof String text)
-        {
-            Object parsed = JSON.parse(text);
-            if (parsed instanceof String nested) parsed = JSON.parse(nested);
-            if (parsed instanceof JSONObject object) return object;
-            return JSON.parseObject(JSON.toJSONString(parsed));
-        }
-        return JSON.parseObject(JSON.toJSONString(value));
-    }
-
-    private JSONArray requiredArray(JSONObject object, String field)
-    {
-        JSONArray array = object.getJSONArray(field);
-        if (array == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
-        return array;
-    }
-
-    private int requiredInteger(JSONObject object, String field)
-    {
-        Integer value = object.getInteger(field);
-        if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
-        return value;
-    }
-
-    private DeviceState deviceState(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        List<ChannelState> channels = new ArrayList<>();
-        requiredArray(object, "channels").forEach(item -> channels.add(channelState(item)));
-        BtsDeviceProfile profile = object.containsKey("profile") ? deviceProfile(object.getJSONObject("profile")) : null;
-        return new DeviceState(requiredInteger(object, "deviceId"), object.getString("ip"),
-                object.getBooleanValue("online"), channels, profile);
-    }
-
-    private BtsDeviceProfile deviceProfile(JSONObject object)
-    {
-        if (object == null) throw new IllegalStateException("Invalid BTS simulator device profile");
-        return new BtsDeviceProfile(requiredInteger(object, "devtype"), requiredInteger(object, "subDeviceId"),
-                object.getString("name"), object.getString("manufacturer"), object.getString("model"),
-                object.getString("serialNumber"), object.getString("firmwareVersion"),
-                requiredInteger(object, "channelCount"), requiredDecimal(object, "currentMinA"),
-                requiredDecimal(object, "currentMaxA"), requiredDecimal(object, "voltageMinV"),
-                requiredDecimal(object, "voltageMaxV"), requiredInteger(object, "sampleIntervalMs"));
-    }
-
-    private CollectorState collectorState(JSONObject object)
-    {
-        int count = requiredInteger(object, "channelCount");
-        List<CollectorChannelState> channels = new ArrayList<>();
-        JSONArray configured = object.getJSONArray("channels");
-        if (configured == null) channels.addAll(com.battery.device.simulator.DevelopmentCollectorSimulation.defaultChannels(count));
-        else configured.forEach(item -> channels.add(collectorChannelState(item)));
-        // 重启只恢复配置,不把上次响应或已废止的历史作为新观测。
-        return new CollectorState(requiredInteger(object, "collectorId"), object.getString("deviceCode"),
-                object.getString("ip"), object.getString("mac"), object.getString("name"),
-                object.getString("manufacturer"), object.getString("model"), count, object.getBooleanValue("online"),
-                com.battery.device.simulator.DevelopmentCollectorSimulation.DEFAULT_SAMPLE_INTERVAL_MS, channels, List.of());
-    }
-
-    private CollectorChannelState collectorChannelState(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        List<CollectorMetricState> metrics = new ArrayList<>();
-        requiredArray(object, "metrics").forEach(item -> metrics.add(collectorMetricState(item)));
-        return new CollectorChannelState(object.getString("channelCode"), metrics,
-                object.getString("workStatus") == null ? DeviceWorkStatus.WORKING
-                        : DeviceWorkStatus.valueOf(object.getString("workStatus")));
-    }
-
-    private CollectorMetricState collectorMetricState(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        BigDecimal measurement = object.getBigDecimal("value");
-        if (measurement == null) throw new IllegalStateException("Missing BTS simulator state field: value");
-        return new CollectorMetricState(object.getString("metricCode"), measurement, object.getString("unit"));
-    }
-
-    private ChannelState channelState(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        String workStatus = object.getString("workStatus");
-        if (workStatus == null) throw new IllegalStateException("Missing BTS simulator state field: workStatus");
-        JSONObject configuration = object.getJSONObject("configuration");
-        return new ChannelState(requiredInteger(object, "channelId"), object.getBooleanValue("online"),
-                DeviceWorkStatus.valueOf(workStatus), channelConfiguration(configuration));
-    }
-
-    /** Redis 通用反序列化会把泛型 steps 恢复为 JSONObject,不能让 record 构造器直接强转。 */
-    private ChannelConfiguration channelConfiguration(JSONObject object)
-    {
-        if (object == null) return null;
-        JSONArray configuredSteps = requiredArray(object, "steps");
-        List<SimulatorPlanStep> steps = new ArrayList<>();
-        configuredSteps.forEach(item -> {
-            JSONObject step = jsonObject(item);
-            steps.add(new SimulatorPlanStep(step.getString("name"), step.getString("stepType"),
-                    step.getDoubleValue("durationSeconds"), step.getDoubleValue("voltageV")));
-        });
-        return new ChannelConfiguration(requiredDecimal(object, "currentA"), requiredDecimal(object, "voltageV"),
-                requiredDecimal(object, "capacityAh"), requiredDecimal(object, "energyWh"),
-                requiredInteger(object, "cycleNo"), requiredInteger(object, "stepNo"),
-                requiredInteger(object, "plannedCycleCount"), object.getLongValue("totalTimeSeconds"),
-                object.getLongValue("relativeTimeSeconds"), steps);
-    }
-
-    private ChannelTombstone channelTombstone(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        return new ChannelTombstone(requiredInteger(object, "deviceId"), object.getString("ip"),
-                requiredInteger(object, "channelId"), requiredInteger(object, "remainingDiscoveries"));
-    }
-
-    private BigDecimal requiredDecimal(JSONObject object, String field)
-    {
-        BigDecimal value = object.getBigDecimal(field);
-        if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
-        return value;
-    }
-
-
-}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
index ac32c87..f5f6dac 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
@@ -36,31 +36,17 @@
         Map<String, CabinetCandidate> merged = new LinkedHashMap<>();
         current.cabinets().forEach(item -> merged.put(item.deviceCode(), item));
         candidates.forEach(item -> merged.put(item.deviceCode(), item));
-        save(new StoredState(CURRENT_VERSION, List.copyOf(merged.values()), current.collectors()));
+        save(new StoredState(CURRENT_VERSION, List.copyOf(merged.values())));
     }
 
-    /** 替换指定设备适配器实例的全部采集器候选,其它实例的候选保持不变。 */
-    @Override
-    public synchronized void replaceCollectors(String adapterCode, List<CollectorCandidate> candidates)
-    {
-        StoredState current = load();
-        List<CollectorCandidate> retained = current.collectors().stream()
-                .filter(item -> !item.adapterCode().equals(adapterCode)).toList();
-        java.util.ArrayList<CollectorCandidate> merged = new java.util.ArrayList<>(retained);
-        merged.addAll(candidates);
-        save(new StoredState(CURRENT_VERSION, current.cabinets(), merged));
-    }
-
-    /** 删除指定设备适配器实例的检测柜与采集器候选;仅供人工重置或实机联调清理。 */
+    /** 删除指定设备适配器实例的检测柜候选;仅供人工重置或实机联调清理。 */
     @Override
     public synchronized void removeAdapter(String adapterCode)
     {
         StoredState current = load();
         List<CabinetCandidate> cabinets = current.cabinets().stream()
                 .filter(item -> !item.adapterCode().equals(adapterCode)).toList();
-        List<CollectorCandidate> collectors = current.collectors().stream()
-                .filter(item -> !item.adapterCode().equals(adapterCode)).toList();
-        save(new StoredState(CURRENT_VERSION, cabinets, collectors));
+        save(new StoredState(CURRENT_VERSION, cabinets));
     }
 
     /**
@@ -74,17 +60,14 @@
         StoredState current = load();
         List<CabinetCandidate> cabinets = current.cabinets().stream()
                 .filter(item -> item.expiresAt().isAfter(now)).toList();
-        List<CollectorCandidate> collectors = current.collectors().stream()
-                .filter(item -> item.expiresAt().isAfter(now)).toList();
-        if (cabinets.size() != current.cabinets().size() || collectors.size() != current.collectors().size())
-            save(new StoredState(CURRENT_VERSION, cabinets, collectors));
-        return new CandidateSnapshot(cabinets, collectors);
+        if (cabinets.size() != current.cabinets().size()) save(new StoredState(CURRENT_VERSION, cabinets));
+        return new CandidateSnapshot(cabinets);
     }
 
     private StoredState load()
     {
         Object stored = redisTemplate.opsForValue().get(STATE_KEY);
-        if (stored == null) return new StoredState(CURRENT_VERSION, List.of(), List.of());
+        if (stored == null) return new StoredState(CURRENT_VERSION, List.of());
         try
         {
             // 通用 RedisTemplate 可能把泛型数组恢复为 JSONObject;在存储边界逐字段重建,禁止业务层强转。
@@ -93,10 +76,9 @@
             if (version == null || version != CURRENT_VERSION)
                 throw new IllegalStateException("Unsupported device discovery state version: " + version);
             List<CabinetCandidate> cabinets = new ArrayList<>();
+            // collectors 是模拟器时代写进同一 key 的历史字段:读到就忽略,不再恢复也不回写。
             requiredArray(root, "cabinets").forEach(item -> cabinets.add(cabinetCandidate(item)));
-            List<CollectorCandidate> collectors = new ArrayList<>();
-            requiredArray(root, "collectors").forEach(item -> collectors.add(collectorCandidate(item)));
-            return new StoredState(CURRENT_VERSION, cabinets, collectors);
+            return new StoredState(CURRENT_VERSION, cabinets);
         }
         catch (IllegalStateException exception)
         {
@@ -155,16 +137,6 @@
                 latestSnapshot, optionalInstant(object, "lastDataAt"), object.getString("sourceDisplayNo"));
     }
 
-    private CollectorCandidate collectorCandidate(Object value)
-    {
-        JSONObject object = jsonObject(value);
-        return new CollectorCandidate(object.getString("adapterCode"), object.getString("deviceCode"),
-                object.getString("ip"), object.getString("mac"), object.getString("name"),
-                object.getString("manufacturer"), object.getString("model"), object.getIntValue("channelCount"),
-                object.getBooleanValue("online"), requiredInstant(object, "observedAt"),
-                requiredInstant(object, "expiresAt"));
-    }
-
     private Instant requiredInstant(JSONObject object, String field)
     {
         Instant value = object.getObject(field, Instant.class);
@@ -183,13 +155,11 @@
         redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
     }
 
-    private record StoredState(int version, List<CabinetCandidate> cabinets,
-            List<CollectorCandidate> collectors)
+    private record StoredState(int version, List<CabinetCandidate> cabinets)
     {
         private StoredState
         {
             cabinets = List.copyOf(cabinets);
-            collectors = List.copyOf(collectors);
         }
     }
 }
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
index 3f7e7d5..0aa0b07 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
@@ -7,8 +7,7 @@
 /**
  * 按固定周期触发数据接收,由业务服务按测试接收状态和采样来源保存成功观测。
  *
- * <p>它不限定 profile:真实来源的接收路径也必须在非开发 profile 下运行。
- * 开发模拟器专属的调度(模拟器发现轮询、采集器状态同步)仍只在 dev-simulator 下额外启用。
+ * <p>它不限定 profile:真实来源的接收路径必须在任何启动方式下都运行。
  */
 @Component
 public class TestDataReceptionScheduler
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TCollectorController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TCollectorController.java
index 0655948..1e9ea28 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TCollectorController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TCollectorController.java
@@ -1,6 +1,5 @@
 package com.battery.business.web;
 
-import java.util.Collections;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -15,15 +14,10 @@
 import org.springframework.web.bind.annotation.RestController;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
-import com.ruoyi.common.constant.HttpStatus;
 import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.core.page.PageDomain;
 import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.common.core.page.TableSupport;
 import com.ruoyi.common.enums.BusinessType;
 import com.battery.business.domain.TCollector;
-import com.battery.business.domain.vo.DiscoveredCollectorVo;
-import com.battery.business.service.ICollectorDiscoveryService;
 import com.battery.business.service.ITCollectorService;
 
 /**
@@ -38,9 +32,6 @@
     @Autowired
     private ITCollectorService collectorService;
 
-    @Autowired
-    private ICollectorDiscoveryService collectorDiscoveryService;
-
     /** 分页查询采集器列表。 */
     @PreAuthorize("@ss.hasPermi('system:collector:list')")
     @GetMapping("/list")
@@ -49,31 +40,6 @@
         startPage();
         List<TCollector> list = collectorService.selectCollectorList(collector);
         return getDataTable(list);
-    }
-
-    /**
-     * 查询当前可发现的采集器列表。
-     *
-     * @return 可发现采集器分页数据
-     */
-    @PreAuthorize("@ss.hasPermi('system:collector:list')")
-    @GetMapping("/discovered/list")
-    public TableDataInfo discoveredList()
-    {
-        List<DiscoveredCollectorVo> devices = collectorDiscoveryService.selectDiscoveredCollectorList();
-        PageDomain pageDomain = TableSupport.buildPageRequest();
-        int pageNum = pageDomain.getPageNum() == null || pageDomain.getPageNum() < 1 ? 1 : pageDomain.getPageNum();
-        int pageSize = pageDomain.getPageSize() == null || pageDomain.getPageSize() < 1 ? 10 : pageDomain.getPageSize();
-        long offset = (long) (pageNum - 1) * pageSize;
-        int fromIndex = (int) Math.min(offset, devices.size());
-        int toIndex = (int) Math.min((long) fromIndex + pageSize, devices.size());
-        List<DiscoveredCollectorVo> rows = fromIndex == toIndex
-                ? Collections.emptyList()
-                : devices.subList(fromIndex, toIndex);
-        TableDataInfo result = new TableDataInfo(rows, devices.size());
-        result.setCode(HttpStatus.SUCCESS);
-        result.setMsg("查询成功");
-        return result;
     }
 
     /** 查询采集器详情。 */
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/device/BtsDevelopmentSimulatorController.java b/ruoyi-admin/src/main/java/com/battery/business/web/device/BtsDevelopmentSimulatorController.java
deleted file mode 100644
index 1aa60ae..0000000
--- a/ruoyi-admin/src/main/java/com/battery/business/web/device/BtsDevelopmentSimulatorController.java
+++ /dev/null
@@ -1,311 +0,0 @@
-package com.battery.business.web.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.http.MediaType;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-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.RestController;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Profile;
-import com.ruoyi.common.annotation.Anonymous;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.simulator.BtsDevelopmentSimulator.StateSnapshot;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
-import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
-
-/** 本机开发模拟器的配置与观测 API,不向设备发送控制命令。 */
-@Anonymous
-@RestController
-@Profile("dev-simulator")
-public class BtsDevelopmentSimulatorController
-{
-    private final BtsDevelopmentSimulator simulator;
-    private final DeviceDiscoveryCandidateStore candidateStore;
-    private final com.battery.business.runtime.DevelopmentCollectorStatusSynchronizer statusSynchronizer;
-
-    @Autowired
-    public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator,
-            DeviceDiscoveryCandidateStore candidateStore,
-            com.battery.business.runtime.DevelopmentCollectorStatusSynchronizer statusSynchronizer)
-    {
-        this.simulator = simulator;
-        this.candidateStore = candidateStore;
-        this.statusSynchronizer = statusSynchronizer;
-    }
-
-    /** 保留独立模拟器测试和工具调用的构造入口,不触发数据库同步。 */
-    public BtsDevelopmentSimulatorController(BtsDevelopmentSimulator simulator,
-            DeviceDiscoveryCandidateStore candidateStore)
-    {
-        this(simulator, candidateStore, null);
-    }
-
-    /** 返回模拟器配置页;匿名放行后仍由本机拦截器限制来源。 */
-    @GetMapping(value = "/dev/bts-simulator.html", produces = MediaType.TEXT_HTML_VALUE)
-    public Resource page()
-    {
-        return new ClassPathResource("static/dev/bts-simulator.html");
-    }
-
-    /** 返回模拟器、虚拟设备、通道和最近请求的当前快照。 */
-    @GetMapping("/dev/bts-simulator/state")
-    public StateSnapshot state()
-    {
-        return simulator.snapshot();
-    }
-
-    /** 人工清空模拟配置并恢复默认检测柜和采集器。 */
-    @PostMapping("/dev/bts-simulator/reset")
-    public StateSnapshot reset()
-    {
-        simulator.reset();
-        candidateStore.removeAdapter(BtsDevelopmentSimulator.ADAPTER_CODE);
-        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
-        {
-            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)
-        {
-            throw badRequest(exception);
-        }
-    }
-
-    /** 修改指定虚拟设备的地址、通道数和在线状态。 */
-    @PutMapping("/dev/bts-simulator/devices/{deviceId}")
-    public StateSnapshot updateDevice(@PathVariable int deviceId, @RequestBody DeviceStateCommand command)
-    {
-        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)
-        {
-            throw badRequest(exception);
-        }
-    }
-
-    /** 删除指定虚拟设备。 */
-    @DeleteMapping("/dev/bts-simulator/devices/{deviceId}")
-    public StateSnapshot deleteDevice(@PathVariable int deviceId)
-    {
-        try
-        {
-            simulator.deleteDevice(deviceId);
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception)
-        {
-            throw badRequest(exception);
-        }
-    }
-
-    /** 添加一台与检测柜分开配置的虚拟采集器。 */
-    @PostMapping("/dev/bts-simulator/collectors")
-    public StateSnapshot createCollector(@RequestBody CreateCollectorCommand command)
-    {
-        try
-        {
-            simulator.addCollector(command.ip(), command.channelCount());
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception)
-        {
-            throw badRequest(exception);
-        }
-    }
-
-    /** 修改虚拟采集器的地址、通道数和在线状态。 */
-    @PutMapping("/dev/bts-simulator/collectors/{collectorId}")
-    public StateSnapshot updateCollector(@PathVariable int collectorId,
-            @RequestBody CollectorStateCommand command)
-    {
-        try
-        {
-            simulator.updateCollector(collectorId, command.ip(), command.channelCount(), command.online());
-            synchronizeOnlineStatusIfConfigured();
-            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);
-        }
-    }
-
-    /** 删除指定虚拟采集器。 */
-    @DeleteMapping("/dev/bts-simulator/collectors/{collectorId}")
-    public StateSnapshot deleteCollector(@PathVariable int collectorId)
-    {
-        try
-        {
-            simulator.deleteCollector(collectorId);
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception)
-        {
-            throw badRequest(exception);
-        }
-    }
-
-    /** 修改指定虚拟设备通道的在线和工作状态。 */
-    @PutMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}")
-    public StateSnapshot updateChannel(@PathVariable int deviceId, @PathVariable int channelId,
-            @RequestBody ChannelStateCommand command)
-    {
-        try
-        {
-            simulator.updateChannel(deviceId, channelId, command.online(), command.workStatus());
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception)
-        {
-            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, exception.getMessage(), exception);
-        }
-    }
-
-    /** 保存单通道的当前观测参数和工步方案,不改变设备在线或工作状态。 */
-    @PutMapping("/dev/bts-simulator/devices/{deviceId}/channels/{channelId}/configuration")
-    public StateSnapshot configureChannel(@PathVariable int deviceId, @PathVariable int channelId,
-            @RequestBody com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration configuration)
-    {
-        try {
-            simulator.configureChannel(deviceId, channelId, configuration);
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception) { throw badRequest(exception); }
-    }
-
-    /** 修改软件可访问性,设备配置继续保留。 */
-    @PutMapping("/dev/bts-simulator/software")
-    public StateSnapshot software(@RequestBody SoftwareCommand command)
-    {
-        if (command == null || command.accessible() == null)
-            throw badRequest(new IllegalArgumentException("accessible must be true or false"));
-        simulator.setSoftwareAccessible(command.accessible());
-        synchronizeOnlineStatusIfConfigured();
-        return simulator.snapshot();
-    }
-
-    /** 修改采集器通道当前工作状态。 */
-    @PutMapping("/dev/bts-simulator/collectors/{collectorId}/channels/{channelCode}/status")
-    public StateSnapshot collectorChannelStatus(@PathVariable int collectorId, @PathVariable String channelCode,
-            @RequestBody CollectorChannelStatusCommand command)
-    {
-        try
-        {
-            simulator.updateCollectorChannelStatus(collectorId, channelCode, command.workStatus());
-            return simulator.snapshot();
-        }
-        catch (IllegalArgumentException exception) { throw badRequest(exception); }
-    }
-
-    private ResponseStatusException badRequest(IllegalArgumentException exception)
-    {
-        return new ResponseStatusException(HttpStatus.BAD_REQUEST, exception.getMessage(), exception);
-    }
-
-    private void synchronizeOnlineStatusIfConfigured()
-    {
-        if (statusSynchronizer != null) statusSynchronizer.synchronizeOnlineStatus();
-    }
-
-    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 CollectorChannelCommand(List<CollectorMetricCommand> metrics) { }
-
-    public record CollectorMetricCommand(String metricCode, BigDecimal value, String unit) { }
-
-    public record SoftwareCommand(Boolean accessible) { }
-
-    public record CollectorChannelStatusCommand(DeviceWorkStatus workStatus) { }
-}
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index dd250f8..c99b10a 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -61,7 +61,7 @@
     # 国际化资源文件路径
     basename: i18n/messages
   profiles:
-    # 默认不连接任何设备;本机演示时显式追加 dev-simulator。
+    # 默认不连接任何设备:没有配置来源时使用无设备实现;真实来源在适配器实例里按行配置。
     active: druid
   # 文件上传
   servlet:
diff --git a/ruoyi-admin/src/main/resources/static/dev/bts-simulator.html b/ruoyi-admin/src/main/resources/static/dev/bts-simulator.html
deleted file mode 100644
index a0ae882..0000000
--- a/ruoyi-admin/src/main/resources/static/dev/bts-simulator.html
+++ /dev/null
@@ -1,660 +0,0 @@
-<!doctype html>
-<html lang="zh-CN">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title>开发设备模拟器</title>
-  <style>
-    :root { color-scheme: light; font-family: "Microsoft YaHei", Arial, sans-serif; color: #26323d; background: #f3f6f8; }
-    * { box-sizing: border-box; }
-    body { margin: 0; min-width: 320px; }
-    header { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 62px; padding: 0 28px; color: #fff; background: #213547; border-bottom: 3px solid #26a269; }
-    h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: 0; }
-    header span { font: 12px Consolas, monospace; color: #c9d5de; }
-    main { width: min(1120px, calc(100% - 32px)); margin: 22px auto 40px; }
-    section { margin-bottom: 20px; background: #fff; border: 1px solid #d5dde3; }
-    .section-title, .device-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 0 16px; background: #edf2f5; border-bottom: 1px solid #d5dde3; }
-    h2, h3 { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: 0; }
-    .summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); }
-    .summary > div { min-height: 76px; padding: 15px 16px; border-right: 1px solid #e2e8ec; }
-    .summary > div:last-child { border-right: 0; }
-    .label { display: block; margin-bottom: 7px; color: #6b7780; font-size: 12px; }
-    .value { font-size: 14px; font-weight: 600; }
-    .status { display: inline-flex; align-items: center; gap: 7px; }
-    .status::before { width: 8px; height: 8px; content: ""; border-radius: 50%; background: #c43d4b; }
-    .status.online::before { background: #218a5b; }
-    .add-form, .device-settings { display: grid; grid-template-columns: minmax(180px, 1fr) 150px auto; gap: 12px; align-items: end; padding: 16px; }
-    .device-settings { grid-template-columns: minmax(130px, 1fr) 90px minmax(150px, 1fr) 100px auto auto; }
-    .collector-settings { display: grid; grid-template-columns: minmax(130px, 1fr) 90px 110px auto auto; gap: 12px; align-items: end; padding: 16px; }
-
-    label { color: #5e6a73; font-size: 12px; }
-    label > span { display: block; margin-bottom: 6px; }
-    input[type="text"], input[type="number"], select, textarea { width: 100%; height: 34px; padding: 0 9px; color: #26323d; background: #fff; border: 1px solid #bdc8d0; border-radius: 3px; }
-    textarea { min-height: 64px; padding: 7px 9px; resize: vertical; font: 12px Consolas, monospace; }
-    input[type="checkbox"] { width: 17px; height: 17px; accent-color: #218a5b; cursor: pointer; }
-    .switch-label { display: inline-flex; align-items: center; gap: 8px; height: 34px; }
-    .switch-label > span { margin: 0; }
-    button { height: 34px; padding: 0 13px; color: #fff; background: #2877a9; border: 1px solid #2877a9; border-radius: 3px; cursor: pointer; }
-    button:hover { background: #1f638e; }
-    button:disabled { cursor: not-allowed; opacity: .5; }
-    button.danger { color: #a03946; background: #fff; border-color: #d8a6ad; }
-    button.danger:hover { background: #fff3f4; }
-    .power-actions { display: flex; flex-wrap: wrap; gap: 8px; }
-    .device { margin: 16px; border: 1px solid #d5dde3; }
-    .device-title { background: #f7f9fa; }
-    table { width: 100%; border-collapse: collapse; table-layout: fixed; }
-    th, td { height: 48px; padding: 8px 14px; text-align: left; border-bottom: 1px solid #e3e9ed; font-size: 13px; }
-    th { height: 40px; color: #5e6a73; background: #f8fafb; font-size: 12px; font-weight: 600; }
-    tbody tr:last-child td { border-bottom: 0; }
-    td select { width: 150px; }
-    .request-time { width: 185px; font: 12px Consolas, monospace; color: #53616b; }
-    .request-command { width: 130px; font: 12px Consolas, monospace; font-weight: 600; }
-    .request-xml { overflow: hidden; color: #5c6871; font: 11px Consolas, monospace; text-overflow: ellipsis; white-space: pre-wrap; overflow-wrap: anywhere; }
-    .collector-actions { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #e3e9ed; }
-    .collector-output { min-height: 46px; margin: 0 16px 16px; padding: 10px; overflow: auto; color: #34434d; background: #f7f9fa; border: 1px solid #d5dde3; font: 11px Consolas, monospace; white-space: pre-wrap; }
-    .empty { height: 96px; color: #7d8992; text-align: center; }
-    .error { display: none; margin-bottom: 16px; padding: 11px 14px; color: #8b2635; background: #fff0f2; border: 1px solid #e4a8b0; font-size: 13px; }
-    .error.visible { display: block; }
-    dialog { width: min(940px, calc(100% - 24px)); max-height: calc(100% - 32px); padding: 0; border: 1px solid #bdc8d0; border-radius: 4px; color: #26323d; }
-    dialog::backdrop { background: rgb(0 0 0 / 35%); }
-    .editor-body { padding: 16px; }
-    .editor-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
-    .editor-scroll { overflow-x: auto; margin-top: 12px; }
-    .editor-scroll table { min-width: 700px; }
-    .editor-scroll td { padding: 6px; }
-    .editor-scroll th { padding: 8px 6px; white-space: nowrap; }
-    .editor-scroll select { width: 100%; }
-    .editor-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 16px; border-top: 1px solid #d5dde3; }
-    .icon-button { width: 30px; padding: 0; color: #52616c; background: #fff; border-color: #bdc8d0; }
-    .row-actions { display: flex; gap: 4px; }
-    .editor-error { margin: 0 0 12px; }
-    @media (max-width: 760px) {
-      header { align-items: flex-start; flex-direction: column; padding: 14px 16px; }
-      main { width: calc(100% - 20px); margin-top: 12px; }
-      .summary { grid-template-columns: 1fr; }
-      .summary > div { border-right: 0; border-bottom: 1px solid #e2e8ec; }
-      .add-form, .device-settings, .collector-settings { grid-template-columns: 1fr 1fr; }
-      .device-settings button, .collector-settings button { width: 100%; }
-      .device { margin: 10px; overflow-x: auto; }
-      .device table { min-width: 620px; }
-      .section-title, .device-title { flex-wrap: wrap; padding: 10px; }
-      .device-title .label { overflow-wrap: anywhere; }
-      th, td { padding: 8px; }
-      .request-time { width: 130px; }
-      .request-command { width: 90px; }
-      .editor-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-    }
-  </style>
-</head>
-<body>
-  <header><h1>开发设备模拟器</h1><span>dev-sim / 仅限本机</span></header>
-  <main>
-    <div id="error" class="error"></div>
-    <section>
-      <div class="section-title">
-        <h2>软件连接</h2>
-        <div class="power-actions">
-          <label class="switch-label"><input id="software-accessible" type="checkbox"><span>新威软件可访问</span></label>
-          <button id="power-on" type="button">全部设备上线</button>
-          <button id="power-off" class="danger" type="button">全部设备离线</button>
-          <button id="reset-simulator" class="danger" type="button">重置模拟器</button>
-        </div>
-      </div>
-      <div class="summary">
-        <div><span class="label">模拟器服务</span><span id="runtime" class="value status">未知</span></div>
-        <div><span class="label">现场设备</span><span id="device-power" class="value status">未知</span></div>
-        <div><span class="label">监听地址</span><span id="address" class="value">-</span></div>
-        <div><span class="label">检测柜</span><span id="device-count" class="value">0 台</span></div>
-        <div><span class="label">采集器</span><span id="collector-count" class="value">0 台</span></div>
-      </div>
-    </section>
-    <section>
-      <div class="section-title"><h2>添加检测柜</h2></div>
-      <form id="add-device" class="add-form">
-        <label><span>虚拟 IP</span><input id="new-ip" type="text" value="127.0.0.3" required></label>
-        <label><span>通道数</span><input id="new-channel-count" type="number" min="1" max="99" value="4" required></label>
-        <label><span>设备 profile</span><select id="new-device-model"><option value="">开发虚拟柜</option><option value="CT-4008T-5V6A-S1-F">新威 CT-4008T-5V6A</option><option value="CT-4008Tn-5V12A-S1-F">新威 CT-4008Tn-5V12A</option></select></label>
-        <button type="submit">添加检测柜</button>
-      </form>
-    </section>
-    <section>
-      <div class="section-title"><h2>检测柜与通道</h2><span class="label">约在下一轮设备发现后同步至业务页面</span></div>
-      <div id="devices"></div>
-    </section>
-    <section>
-      <div class="section-title"><h2>添加采集器</h2><span class="label">与检测柜分开配置</span></div>
-      <form id="add-collector" class="add-form">
-        <label><span>虚拟 IP</span><input id="new-collector-ip" type="text" value="127.0.1.3" required></label>
-        <label><span>通道数</span><input id="new-collector-channel-count" type="number" min="1" max="99" value="4" required></label>
-        <button type="submit">添加采集器</button>
-      </form>
-    </section>
-    <section>
-      <div class="section-title"><h2>采集器</h2><span class="label">在采集器页面“选择设备”中直接显示</span></div>
-      <div id="collectors"></div>
-    </section>
-    <section>
-      <div class="section-title"><h2>最近请求</h2><span id="request-count" class="label">0 条</span></div>
-      <table>
-        <thead><tr><th class="request-time">接收时间</th><th class="request-command">命令</th><th>请求 / 响应</th></tr></thead>
-        <tbody id="requests"></tbody>
-      </table>
-    </section>
-  </main>
-  <dialog id="channel-editor" aria-labelledby="editor-title">
-    <form id="editor-form">
-      <div class="section-title"><h3 id="editor-title"></h3><button class="icon-button" type="button" id="editor-close" aria-label="关闭配置" title="关闭配置">×</button></div>
-      <div class="editor-body"><div id="editor-error" class="error editor-error" role="alert"></div><div id="editor-content"></div></div>
-      <div class="editor-actions"><button type="button" id="editor-cancel" class="danger">取消</button><button type="submit" id="editor-save">保存通道配置</button></div>
-    </form>
-  </dialog>
-  <script>
-    const api = '/dev/bts-simulator';
-    const labels = { STOPPED: '空闲', WORKING: '运行', PAUSED: '暂停', FINISHED: '结束', PROTECTED: '异常', UNOBSERVABLE: '无法观测' };
-    const errorBox = document.querySelector('#error');
-    const editor = document.querySelector('#channel-editor');
-    const editorForm = document.querySelector('#editor-form');
-    const editorContent = document.querySelector('#editor-content');
-    const stepTypes = { cc: '恒流充电', dc: '恒流放电', cv: '恒压充电', rest: '搁置', end: '结束', cccv: '恒流恒压充电' };
-    const metricLabels = { temperature: '温度', positive_potential: '正电位', negative_potential: '负电位' };
-    let saveEditor = null;
-    document.querySelector('#editor-close').onclick = () => editor.close();
-    document.querySelector('#editor-cancel').onclick = () => editor.close();
-    editorForm.addEventListener('submit', async event => {
-      event.preventDefault();
-      const save = document.querySelector('#editor-save');
-      save.disabled = true;
-      try { const state = await saveEditor(); editor.close(); render(state); }
-      catch (error) { showError(error); }
-      finally { save.disabled = false; }
-    });
-
-    async function request(path, options) {
-      const response = await fetch(api + path, { ...options, headers: { 'Content-Type': 'application/json' } });
-      if (!response.ok) {
-        let message = `请求失败 (${response.status})`;
-        try { const error = await response.json(); message = error?.msg || error?.message || message; } catch (_) { }
-        throw new Error(message);
-      }
-      const payload = await response.json();
-      if (payload && payload.data && typeof payload.data === 'object'
-        && !Array.isArray(payload.devices) && payload.data.devices) return payload.data;
-      return payload;
-    }
-
-    function showError(error) {
-      errorBox.textContent = error instanceof Error ? error.message : '控制页请求失败';
-      errorBox.classList.add('visible');
-      if (editor.open) {
-        const message = document.querySelector('#editor-error');
-        message.textContent = errorBox.textContent;
-        message.classList.add('visible');
-      }
-    }
-
-    function render(state) {
-      errorBox.classList.remove('visible');
-      if (!state || typeof state !== 'object') throw new Error('模拟器响应缺少状态数据');
-      if (state.data && typeof state.data === 'object' && !Array.isArray(state.devices)) state = state.data;
-      state = {
-        ...state,
-        devices: Array.isArray(state.devices) ? state.devices : [],
-        collectors: Array.isArray(state.collectors) ? state.collectors : [],
-        recentRequests: Array.isArray(state.recentRequests) ? state.recentRequests : [],
-      };
-      document.querySelector('#software-accessible').checked = state.softwareAccessible;
-      const runtime = document.querySelector('#runtime');
-      runtime.textContent = state.running ? '运行中' : '已停止';
-      runtime.classList.toggle('online', state.running);
-      const devicesPoweredOn = state.devices.length + state.collectors.length > 0
-        && state.devices.every(device => device.online)
-        && state.collectors.every(collector => collector.online);
-      const devicesPoweredOff = state.devices.every(device => !device.online)
-        && state.collectors.every(collector => !collector.online);
-      const devicePower = document.querySelector('#device-power');
-      devicePower.textContent = devicesPoweredOn ? '全部在线' : devicesPoweredOff ? '全部离线' : '部分在线';
-      devicePower.classList.toggle('online', devicesPoweredOn);
-      document.querySelector('#power-on').disabled = devicesPoweredOn;
-      document.querySelector('#power-off').disabled = devicesPoweredOff;
-      document.querySelector('#address').textContent = state.listenAddress;
-      document.querySelector('#device-count').textContent = `${state.devices.length} 台`;
-      document.querySelector('#collector-count').textContent = `${state.collectors.length} 台`;
-      renderDevices(state.devices);
-      renderCollectors(state.collectors);
-      renderRequests(state.recentRequests);
-    }
-
-    function renderDevices(devices) {
-      const container = document.querySelector('#devices');
-      if (!devices.length) {
-        const empty = document.createElement('div');
-        empty.className = 'empty';
-        empty.textContent = '暂无虚拟设备';
-        container.replaceChildren(empty);
-        return;
-      }
-      container.replaceChildren(...devices.map(device => {
-        const panel = document.createElement('article');
-        panel.className = 'device';
-        const title = document.createElement('div');
-        title.className = 'device-title';
-        const heading = document.createElement('h3');
-        heading.textContent = `检测柜 ${device.deviceId}`;
-        const identity = document.createElement('span');
-        identity.className = 'label';
-        identity.textContent = `devtype ${device.profile.devtype} / devid ${device.deviceId} / subdevid ${device.profile.subDeviceId} / ${device.profile.model}`;
-        title.append(heading, identity);
-
-        const settings = document.createElement('div');
-        settings.className = 'device-settings';
-        const ip = field('虚拟 IP', 'text', device.ip);
-        const channelCount = field('通道数', 'number', device.channels.length);
-        channelCount.input.min = '1';
-        channelCount.input.max = '99';
-        const model = document.createElement('label');
-        model.innerHTML = '<span>设备 profile</span>';
-        const modelInput = document.createElement('select');
-        modelInput.innerHTML = '<option value="">开发虚拟柜</option><option value="CT-4008T-5V6A-S1-F">新威 CT-4008T-5V6A</option><option value="CT-4008Tn-5V12A-S1-F">新威 CT-4008Tn-5V12A</option>';
-        modelInput.value = device.profile.model.startsWith('CT-4008T') ? device.profile.model : '';
-        model.appendChild(modelInput);
-        const onlineLabel = document.createElement('label');
-        onlineLabel.className = 'switch-label';
-        const online = document.createElement('input');
-        online.type = 'checkbox';
-        online.checked = device.online;
-        const onlineText = document.createElement('span');
-        onlineText.textContent = device.online ? '整机在线' : '整机离线';
-        online.addEventListener('change', () => { onlineText.textContent = online.checked ? '整机在线' : '整机离线'; });
-        onlineLabel.append(online, onlineText);
-        const save = document.createElement('button');
-        save.type = 'button';
-        save.textContent = '保存配置';
-        save.addEventListener('click', async () => {
-          try { render(await request(`/devices/${device.deviceId}`, { method: 'PUT', body: JSON.stringify({ ip: ip.input.value, channelCount: Number(channelCount.input.value), online: online.checked, model: modelInput.value || null }) })); }
-          catch (error) { showError(error); }
-        });
-        const remove = document.createElement('button');
-        remove.type = 'button';
-        remove.className = 'danger';
-        remove.textContent = '删除检测柜';
-        remove.addEventListener('click', async () => {
-          try { render(await request(`/devices/${device.deviceId}`, { method: 'DELETE' })); }
-          catch (error) { showError(error); }
-        });
-        settings.append(ip.label, channelCount.label, model, onlineLabel, save, remove);
-
-        const table = document.createElement('table');
-        table.innerHTML = '<thead><tr><th>通道</th><th>在线状态</th><th>工作状态</th><th>观测配置</th></tr></thead>';
-        const body = document.createElement('tbody');
-        body.append(...device.channels.map(channel => channelRow(device.deviceId, channel)));
-        table.append(body);
-        panel.append(title, settings, table);
-        return panel;
-      }));
-    }
-
-    function renderCollectors(collectors) {
-      const container = document.querySelector('#collectors');
-      if (!collectors.length) {
-        const empty = document.createElement('div');
-        empty.className = 'empty';
-        empty.textContent = '暂无虚拟采集器';
-        container.replaceChildren(empty);
-        return;
-      }
-      container.replaceChildren(...collectors.map(collector => {
-        const panel = document.createElement('article');
-        panel.className = 'device';
-        const title = document.createElement('div');
-        title.className = 'device-title';
-        const heading = document.createElement('h3');
-        heading.textContent = collector.name;
-        const identity = document.createElement('span');
-        identity.className = 'label';
-        identity.textContent = `${collector.deviceCode} / ${collector.mac}`;
-        title.append(heading, identity);
-
-        const settings = document.createElement('div');
-        settings.className = 'collector-settings';
-        const ip = field('虚拟 IP', 'text', collector.ip);
-        const channelCount = field('通道数', 'number', collector.channelCount);
-        channelCount.input.min = '1';
-        channelCount.input.max = '99';
-        const onlineLabel = document.createElement('label');
-        onlineLabel.className = 'switch-label';
-        const online = document.createElement('input');
-        online.type = 'checkbox';
-        online.checked = collector.online;
-        const onlineText = document.createElement('span');
-        onlineText.textContent = collector.online ? '整机在线' : '整机离线';
-        online.addEventListener('change', () => { onlineText.textContent = online.checked ? '整机在线' : '整机离线'; });
-        onlineLabel.append(online, onlineText);
-        const save = document.createElement('button');
-        save.type = 'button';
-        save.textContent = '保存配置';
-        save.addEventListener('click', async () => {
-          try {
-            render(await request(`/collectors/${collector.collectorId}`, { method: 'PUT', body: JSON.stringify({ ip: ip.input.value, channelCount: Number(channelCount.input.value), online: online.checked }) }));
-          }
-          catch (error) { showError(error); }
-        });
-        const remove = document.createElement('button');
-        remove.type = 'button';
-        remove.className = 'danger';
-        remove.textContent = '删除采集器';
-        remove.addEventListener('click', async () => {
-          try { render(await request(`/collectors/${collector.collectorId}`, { method: 'DELETE' })); }
-          catch (error) { showError(error); }
-        });
-        settings.append(ip.label, channelCount.label, onlineLabel, save, remove);
-
-        const table = document.createElement('table');
-        table.innerHTML = '<thead><tr><th style="width:100px">稳定通道</th><th style="width:170px">工作状态</th><th>测量项</th><th style="width:130px">操作</th></tr></thead>';
-        const channelBody = document.createElement('tbody');
-        channelBody.append(...collector.channels.map(channel => collectorChannelRow(collector.collectorId, channel)));
-        table.append(channelBody);
-
-        const actions = document.createElement('div');
-        actions.className = 'collector-actions';
-        const realtimeButton = document.createElement('button');
-        realtimeButton.type = 'button';
-        realtimeButton.textContent = '查看实时';
-        const output = document.createElement('pre');
-        output.className = 'collector-output';
-        output.textContent = collector.latestMeasurements.length
-          ? measurementLines(collector.latestMeasurements)
-          : '暂无成功观测';
-        realtimeButton.addEventListener('click', async () => {
-          try { output.textContent = measurementLines((await request(`/collectors/${collector.collectorId}/realtime`)).measurements); }
-          catch (error) { showError(error); }
-        });
-        actions.append(realtimeButton);
-        panel.append(title, settings, table, actions, output);
-        return panel;
-      }));
-    }
-
-    function collectorChannelRow(collectorId, channel) {
-      const row = document.createElement('tr');
-      const identity = document.createElement('td');
-      identity.textContent = channel.channelCode;
-      const metricsCell = document.createElement('td');
-      metricsCell.textContent = channel.metrics.map(item => `${metricLabels[item.metricCode] || item.metricCode} ${item.value} ${item.unit}`).join(' / ');
-      metricsCell.style.overflowWrap = 'anywhere';
-      const action = document.createElement('td');
-      const save = document.createElement('button');
-      save.type = 'button';
-      save.textContent = '编辑测量值';
-      save.addEventListener('click', () => editCollectorChannel(collectorId, channel));
-      action.append(save);
-      const statusCell = document.createElement('td');
-      const status = document.createElement('select');
-      status.setAttribute('aria-label', channel.channelCode + ' 工作状态');
-      Object.entries(labels).forEach(([value, label]) => status.add(new Option(label, value)));
-      status.value = channel.workStatus;
-      status.addEventListener('change', async () => {
-        try { render(await request(`/collectors/${collectorId}/channels/${channel.channelCode}/status`, { method: 'PUT', body: JSON.stringify({ workStatus: status.value }) })); }
-        catch (error) { showError(error); }
-      });
-      statusCell.append(status);
-      row.append(identity, statusCell, metricsCell, action);
-      return row;
-    }
-
-    function measurementLines(items) {
-      if (!items.length) return '暂无测量';
-      return items.map(item => `${item.collectorCode} #${item.sequenceId} ${item.channelCode} ${item.metricCode}=${item.value} ${item.unit} 设备 ${item.deviceTimeRaw} / 接收 ${item.serverReceivedAt}`).join('\n');
-    }
-
-    function field(labelText, type, value) {
-      const label = document.createElement('label');
-      const text = document.createElement('span');
-      text.textContent = labelText;
-      const input = document.createElement('input');
-      input.type = type;
-      input.value = value;
-      label.append(text, input);
-      return { label, input };
-    }
-
-    function openEditor(title) {
-      editorContent.replaceChildren();
-      document.querySelector('#editor-title').textContent = title;
-      document.querySelector('#editor-error').classList.remove('visible');
-      editor.showModal();
-    }
-
-    function numericField(label, value, integer = false) {
-      const result = field(label, 'number', value);
-      result.input.required = true;
-      result.input.step = integer ? '1' : 'any';
-      return result;
-    }
-
-    function iconButton(symbol, label, action) {
-      const button = document.createElement('button');
-      button.type = 'button'; button.className = 'icon-button'; button.textContent = symbol;
-      button.title = label; button.setAttribute('aria-label', label); button.onclick = action;
-      return button;
-    }
-
-    function editCabinetChannel(deviceId, channel) {
-      openEditor(`检测柜 ${deviceId} · 通道 ${channel.channelId} 参数与方案`);
-      const config = channel.configuration;
-      const fields = document.createElement('div'); fields.className = 'editor-fields';
-      const inputs = {};
-      for (const [key, label, integer] of [
-        ['voltageV', '电压 (V)'], ['currentA', '电流 (A)'], ['capacityAh', '容量 (Ah)'], ['energyWh', '能量 (Wh)'],
-        ['cycleNo', '当前循环', true], ['stepNo', '当前工步', true], ['plannedCycleCount', '计划循环数', true],
-        ['totalTimeSeconds', '总时间 (s)', true], ['relativeTimeSeconds', '工步时间 (s)', true],
-      ]) {
-        const item = numericField(label, config[key], integer);
-        if (integer) item.input.min = key.endsWith('Seconds') ? '0' : '1';
-        inputs[key] = item.input; fields.append(item.label);
-      }
-      const heading = document.createElement('div'); heading.className = 'section-title';
-      const title = document.createElement('h3'); title.textContent = '工步方案';
-      const add = document.createElement('button'); add.type = 'button'; add.textContent = '添加工步';
-      heading.append(title, add);
-      const scroll = document.createElement('div'); scroll.className = 'editor-scroll';
-      const table = document.createElement('table');
-      table.innerHTML = '<thead><tr><th style="width:50px">序号</th><th>工步名称</th><th style="width:140px">工步类型</th><th style="width:110px">时长 (s)</th><th style="width:100px">电压 (V)</th><th style="width:112px">操作</th></tr></thead>';
-      const body = document.createElement('tbody'); table.append(body); scroll.append(table);
-      const steps = [];
-      function syncRows() {
-        body.replaceChildren(...steps.map((step, index) => {
-          step.number.textContent = index + 1;
-          step.up.disabled = index === 0; step.down.disabled = index === steps.length - 1;
-          step.remove.disabled = steps.length === 1;
-          return step.row;
-        }));
-        inputs.stepNo.max = String(steps.length);
-        if (Number(inputs.stepNo.value) > steps.length) inputs.stepNo.value = steps.length;
-      }
-      function addStep(value) {
-        const row = document.createElement('tr'); const number = document.createElement('td'); row.append(number);
-        const name = field('工步名称', 'text', value.name).input; name.required = true; name.setAttribute('aria-label', '工步名称');
-        const type = document.createElement('select'); type.setAttribute('aria-label', '工步类型');
-        Object.entries(stepTypes).forEach(([code, label]) => type.add(new Option(label, code))); type.value = value.stepType;
-        const duration = numericField('时长 (s)', value.durationSeconds).input; duration.min = '0'; duration.setAttribute('aria-label', '时长 (s)');
-        const voltage = numericField('电压 (V)', value.voltageV).input; voltage.setAttribute('aria-label', '工步电压 (V)');
-        for (const input of [name, type, duration, voltage]) { const cell = document.createElement('td'); cell.append(input); row.append(cell); }
-        const step = { row, number, name, type, duration, voltage };
-        const move = delta => { const index = steps.indexOf(step); steps.splice(index, 1); steps.splice(index + delta, 0, step); syncRows(); };
-        step.up = iconButton('↑', '上移工步', () => move(-1));
-        step.down = iconButton('↓', '下移工步', () => move(1));
-        step.remove = iconButton('×', '删除工步', () => { steps.splice(steps.indexOf(step), 1); syncRows(); });
-        const actions = document.createElement('td'); const buttons = document.createElement('div'); buttons.className = 'row-actions';
-        buttons.append(step.up, step.down, step.remove); actions.append(buttons); row.append(actions);
-        steps.push(step); syncRows();
-      }
-      config.steps.forEach(addStep);
-      // 初次填充方案行时保留保存过的当前工步位置。
-      inputs.stepNo.value = config.stepNo;
-      add.onclick = () => addStep({ name: '新工步', stepType: 'rest', durationSeconds: 60, voltageV: 0 });
-      editorContent.append(fields, heading, scroll);
-      saveEditor = () => request(`/devices/${deviceId}/channels/${channel.channelId}/configuration`, {
-        method: 'PUT', body: JSON.stringify({
-          ...Object.fromEntries(Object.entries(inputs).map(([key, input]) => [key, Number(input.value)])),
-          steps: steps.map(step => ({ name: step.name.value, stepType: step.type.value,
-            durationSeconds: Number(step.duration.value), voltageV: Number(step.voltage.value) })),
-        }),
-      });
-    }
-
-    function editCollectorChannel(collectorId, channel) {
-      openEditor(`采集器 ${collectorId} · ${channel.channelCode} 测量值`);
-      const add = document.createElement('button'); add.type = 'button'; add.textContent = '添加测量项';
-      const scroll = document.createElement('div'); scroll.className = 'editor-scroll';
-      const table = document.createElement('table'); table.style.minWidth = '560px';
-      table.innerHTML = '<thead><tr><th>测量项</th><th>数值</th><th style="width:100px">单位</th><th style="width:50px">操作</th></tr></thead>';
-      const body = document.createElement('tbody'); table.append(body); scroll.append(table);
-      const metrics = [];
-      function addMetric(item) {
-        const row = document.createElement('tr');
-        const kind = document.createElement('select'); kind.setAttribute('aria-label', '测量项');
-        Object.entries(metricLabels).forEach(([code, label]) => kind.add(new Option(label, code)));
-        kind.add(new Option('自定义', 'custom')); kind.value = metricLabels[item.metricCode] ? item.metricCode : 'custom';
-        const code = field('自定义名称', 'text', item.metricCode).input; code.setAttribute('aria-label', '自定义名称');
-        const value = numericField('数值', item.value).input; value.setAttribute('aria-label', '数值');
-        const unit = field('单位', 'text', item.unit).input; unit.required = true; unit.setAttribute('aria-label', '单位');
-        const updateKind = () => {
-          code.hidden = kind.value !== 'custom'; code.required = !code.hidden;
-          unit.readOnly = kind.value !== 'custom';
-          if (kind.value !== 'custom') unit.value = kind.value === 'temperature' ? '℃' : 'V';
-        };
-        code.hidden = kind.value !== 'custom'; code.required = !code.hidden;
-        kind.onchange = updateKind;
-        const identity = document.createElement('td'); identity.append(kind, code); row.append(identity);
-        for (const input of [value, unit]) { const cell = document.createElement('td'); cell.append(input); row.append(cell); }
-        const metric = { row, kind, code, value, unit };
-        const action = document.createElement('td');
-        metric.remove = iconButton('×', '删除测量项', () => {
-          metrics.splice(metrics.indexOf(metric), 1); row.remove(); syncButtons();
-        });
-        action.append(metric.remove); row.append(action); metrics.push(metric); body.append(row); syncButtons();
-      }
-      function syncButtons() { metrics.forEach(item => { item.remove.disabled = metrics.length === 1; }); }
-      channel.metrics.forEach(addMetric);
-      add.onclick = () => addMetric({ metricCode: '', value: 0, unit: 'V' });
-      editorContent.append(add, scroll);
-      saveEditor = () => {
-        const values = metrics.map(item => ({ metricCode: item.kind.value === 'custom' ? item.code.value.trim() : item.kind.value,
-          value: Number(item.value.value), unit: item.unit.value.trim() }));
-        if (new Set(values.map(item => item.metricCode)).size !== values.length) throw new Error('测量项不能重复');
-        return request(`/collectors/${collectorId}/channels/${channel.channelCode}`, {
-          method: 'PUT', body: JSON.stringify({ metrics: values }),
-        });
-      };
-    }
-
-    function channelRow(deviceId, channel) {
-      const row = document.createElement('tr');
-      const identity = document.createElement('td');
-      identity.textContent = `通道 ${channel.channelId}`;
-      const onlineCell = document.createElement('td');
-      const onlineLabel = document.createElement('label');
-      onlineLabel.className = 'switch-label';
-      const online = document.createElement('input');
-      online.type = 'checkbox';
-      online.checked = channel.online;
-      const onlineText = document.createElement('span');
-      onlineText.textContent = channel.online ? '在线' : '离线';
-      onlineLabel.append(online, onlineText);
-      onlineCell.append(onlineLabel);
-      const statusCell = document.createElement('td');
-      const status = document.createElement('select');
-      Object.entries(labels).forEach(([value, label]) => status.add(new Option(label, value)));
-      status.value = channel.workStatus;
-      status.setAttribute('aria-label', `检测柜 ${deviceId} 通道 ${channel.channelId} 工作状态`);
-      const update = async () => {
-        try { render(await request(`/devices/${deviceId}/channels/${channel.channelId}`, { method: 'PUT', body: JSON.stringify({ online: online.checked, workStatus: status.value }) })); }
-        catch (error) { showError(error); await refresh(); }
-      };
-      online.addEventListener('change', update);
-      status.addEventListener('change', update);
-      statusCell.append(status);
-      const configCell = document.createElement('td');
-      const configure = document.createElement('button'); configure.type = 'button'; configure.textContent = '参数与方案';
-      configure.onclick = () => editCabinetChannel(deviceId, channel);
-      configCell.append(configure);
-      row.append(identity, onlineCell, statusCell, configCell);
-      return row;
-    }
-
-    function renderRequests(items) {
-      const requests = document.querySelector('#requests');
-      document.querySelector('#request-count').textContent = `${items.length} 条`;
-      if (!items.length) {
-        const row = document.createElement('tr');
-        const cell = document.createElement('td');
-        cell.colSpan = 3;
-        cell.className = 'empty';
-        cell.textContent = '暂无请求';
-        row.append(cell);
-        requests.replaceChildren(row);
-        return;
-      }
-      requests.replaceChildren(...items.map(item => {
-        const row = document.createElement('tr');
-        const time = document.createElement('td');
-        time.className = 'request-time';
-        time.textContent = new Date(item.receivedAt).toLocaleString('zh-CN', { hour12: false });
-        const command = document.createElement('td');
-        command.className = 'request-command';
-        command.textContent = item.command;
-        const xml = document.createElement('td');
-        xml.className = 'request-xml';
-        xml.textContent = `${item.request}\n${item.response || '暂无响应'}`;
-        xml.title = xml.textContent;
-        row.append(time, command, xml);
-        return row;
-      }));
-    }
-
-    async function refresh() {
-      if (editor.open) return;
-      if (document.activeElement && ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
-      try { render(await request('/state')); } catch (error) { showError(error); }
-    }
-
-    document.getElementById('reset-simulator').addEventListener('click', async () => {
-      if (!confirm('确认清空全部模拟配置和状态并恢复默认设备?')) return;
-      try { render(await request('/reset', { method: 'POST' })); } catch (error) { showError(error); }
-    });
-
-    document.getElementById('software-accessible').addEventListener('change', async event => {
-      try { render(await request('/software', { method: 'PUT', body: JSON.stringify({ accessible: event.target.checked }) })); }
-      catch (error) { showError(error); }
-    });
-
-    document.getElementById('power-on').addEventListener('click', async () => {
-      try { render(await request('/power', { method: 'POST', body: JSON.stringify({ poweredOn: true }) })); }
-      catch (error) { showError(error); }
-    });
-
-    document.getElementById('power-off').addEventListener('click', async () => {
-      if (!confirm('确认将全部模拟检测柜和采集器设为离线?')) return;
-      try { render(await request('/power', { method: 'POST', body: JSON.stringify({ poweredOn: false }) })); }
-      catch (error) { showError(error); }
-    });
-
-    document.querySelector('#add-device').addEventListener('submit', async event => {
-      event.preventDefault();
-      try {
-        render(await request('/devices', { method: 'POST', body: JSON.stringify({ ip: document.querySelector('#new-ip').value, channelCount: Number(document.querySelector('#new-channel-count').value), model: document.querySelector('#new-device-model').value || null }) }));
-      } catch (error) { showError(error); }
-    });
-
-    document.querySelector('#add-collector').addEventListener('submit', async event => {
-      event.preventDefault();
-      try {
-        render(await request('/collectors', { method: 'POST', body: JSON.stringify({ ip: document.querySelector('#new-collector-ip').value, channelCount: Number(document.querySelector('#new-collector-channel-count').value) }) }));
-      } catch (error) { showError(error); }
-    });
-
-    refresh();
-    setInterval(refresh, 2000);
-  </script>
-</body>
-</html>
diff --git a/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java b/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
index a6188d6..1f87f3d 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
@@ -61,6 +61,9 @@
             sql.execute("insert into t_detection_cabinet(cabinet_id,cabinet_name,online_status) values (1,'Cabinet','OFFLINE'),(3,'Unused','ONLINE')");
             sql.execute("create table t_collector(collector_id bigint primary key, collector_code varchar(64), collector_name varchar(100), last_seen_at_utc timestamp, " + common + ")");
             sql.execute("insert into t_collector(collector_id,collector_name,online_status) values (1,'Collector','ONLINE'),(3,'Unused','OFFLINE')");
+            // 采集器在线状态由所链接来源实例的连接结果派生(ADR-0063/0064),所以这个表必须在场。
+            sql.execute("create table t_device_adapter(adapter_id bigint primary key, connection_status varchar(16))");
+            sql.execute("insert into t_device_adapter values (1,'ONLINE')");
         }
     }
 
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentSimulatorProfileContractTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/DefaultSpringProfileContractTest.java
similarity index 69%
rename from ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentSimulatorProfileContractTest.java
rename to ruoyi-admin/src/test/java/com/battery/business/runtime/DefaultSpringProfileContractTest.java
index 0033d51..221c1d8 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentSimulatorProfileContractTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/runtime/DefaultSpringProfileContractTest.java
@@ -7,7 +7,11 @@
 import org.springframework.boot.env.YamlPropertySourceLoader;
 import org.springframework.core.io.ClassPathResource;
 
-class DevelopmentSimulatorProfileContractTest
+/**
+ * 默认启动不连接任何设备:没有配置来源时装配无设备实现,真实来源按适配器实例逐行配置。
+ * 这里锁定配置文件里的默认 profile,避免有人在配置里提前打开设备连接。
+ */
+class DefaultSpringProfileContractTest
 {
     @Test
     void usesNoDeviceProfileByDefault() throws Exception
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizerTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizerTest.java
deleted file mode 100644
index a68c9f9..0000000
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/DevelopmentCollectorStatusSynchronizerTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.battery.business.runtime;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.junit.jupiter.api.Test;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.business.mapper.TCollectorMapper;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-class DevelopmentCollectorStatusSynchronizerTest
-{
-    @Test
-    void productionConstructorIsExplicitlyAutowired()
-    {
-        var constructors = DevelopmentCollectorStatusSynchronizer.class.getDeclaredConstructors();
-        assertEquals(1, constructors.length);
-        var production = java.util.Arrays.stream(constructors)
-                .filter(item -> item.isAnnotationPresent(Autowired.class)).findFirst().orElseThrow();
-        assertEquals(3, production.getParameterCount());
-    }
-    @Test
-    void synchronizesOnlyConfiguredCollectorsWithoutRegisteringThem() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-            TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
-            DevelopmentCollectorStatusSynchronizer synchronizer =
-                    new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper);
-            simulator.updateCollector(1, "127.0.1.2", 4, false);
-
-            synchronizer.synchronizeOnlineStatus();
-
-            assertEquals(false, store.snapshot(java.time.Instant.now()).collectors().get(0).online());
-            verify(collectorMapper).updateRegisteredObservation("dev-sim-collector-1", "OFFLINE");
-
-            simulator.deleteCollector(1);
-            synchronizer.synchronizeOnlineStatus();
-
-            assertEquals(0, store.snapshot(java.time.Instant.now()).collectors().size());
-        }
-    }
-}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeConfigTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeConfigTest.java
deleted file mode 100644
index 99f5714..0000000
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeConfigTest.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.battery.business.runtime;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.DefaultApplicationArguments;
-import org.mockito.ArgumentCaptor;
-import com.battery.device.api.registry.AdapterInstanceConfig;
-import com.battery.device.api.registry.DeviceAdapterRegistry;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-
-class DeviceAdapterRuntimeConfigTest
-{
-    @Test
-    void registersDevelopmentSimulatorInstanceBeforeApplicationReady() throws Exception
-    {
-        DeviceAdapterRegistry registry = mock(DeviceAdapterRegistry.class);
-        BtsDevelopmentSimulator simulator = mock(BtsDevelopmentSimulator.class);
-        var runner = new DevelopmentSimulatorConfig().registerDevelopmentSimulatorAdapter(registry, simulator, 15021);
-
-        runner.run(new DefaultApplicationArguments(new String[0]));
-
-        // 注册模拟器对象本身:发现与取数落在同一实例上,不再单独注入扩展观测 Bean。
-        ArgumentCaptor<AdapterInstanceConfig> adapter = ArgumentCaptor.forClass(AdapterInstanceConfig.class);
-        verify(registry).register(adapter.capture(), org.mockito.ArgumentMatchers.same(simulator));
-        assertEquals("dev-sim", adapter.getValue().adapterCode());
-        assertEquals("127.0.0.1", adapter.getValue().host());
-        assertEquals(15021, adapter.getValue().port());
-        assertEquals(5000, adapter.getValue().pollIntervalMs());
-    }
-}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeNoopStartupTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeNoopStartupTest.java
index 91d338c..f3c364c 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeNoopStartupTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/runtime/DeviceAdapterRuntimeNoopStartupTest.java
@@ -22,6 +22,7 @@
             context.registerBean(TDeviceIngestRecordMapper.class, () -> mock(TDeviceIngestRecordMapper.class));
             context.refresh();
             DeviceAdapterRegistry registry = context.getBean(DeviceAdapterRegistry.class);
+            // 模拟器已整体拆除:它的运行时编码不再出现在注册表里。
             assertFalse(registry.contains("dev-sim"));
             registry.synchronize(new com.battery.device.api.registry.AdapterInstanceConfig(
                     "noop-check", "NOOP", "127.0.0.1", 1, 1000, "Asia/Hong_Kong",
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/RedisStateStoreTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/RedisStateStoreTest.java
deleted file mode 100644
index 8023b0d..0000000
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/RedisStateStoreTest.java
+++ /dev/null
@@ -1,298 +0,0 @@
-package com.battery.business.runtime;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicReference;
-import org.junit.jupiter.api.Test;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.ListOperations;
-import org.springframework.data.redis.core.SessionCallback;
-import org.springframework.data.redis.core.SetOperations;
-import org.springframework.data.redis.core.ValueOperations;
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONObject;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
-import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
-import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
-import com.battery.device.api.model.ChannelIdentity;
-import com.ruoyi.framework.config.FastJson2JsonRedisSerializer;
-
-class RedisStateStoreTest
-{
-    @Test
-    void channelParametersAndPlanSurviveRedisRestartAndOnlineChanges() throws Exception
-    {
-        var stored = new AtomicReference<Object>();
-        var configuration = new com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration(
-                new java.math.BigDecimal("-3.123456"), new java.math.BigDecimal("8.125"),
-                new java.math.BigDecimal("7.2"), new java.math.BigDecimal("29.7"), 2, 2, 5, 120, 30,
-                List.of(new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("充电", "cc", 60, 4.2),
-                        new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("放电 <验证>", "dc", 90, 3.0)));
-        try (var simulator = new BtsDevelopmentSimulator(0, store(stored))) {
-            simulator.configureChannel(1, 1, configuration);
-            simulator.updateChannel(1, 1, false, DeviceWorkStatus.PAUSED);
-            simulator.setAllDevicesPoweredOn(false);
-            simulator.setSoftwareAccessible(false);
-        }
-        try (var simulator = new BtsDevelopmentSimulator(0, store(stored))) {
-            assertEquals(configuration, simulator.snapshot().devices().get(0).channels().get(0).configuration());
-            simulator.setSoftwareAccessible(true);
-            simulator.setAllDevicesPoweredOn(true);
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
-            var identity = simulator.discoverChannels().get(0).identity();
-            assertEquals(-3.123456, simulator.latestCabinet(identity).currentA());
-            assertEquals(8.125, simulator.latestCabinet(identity).voltageV());
-            assertEquals("放电 <验证>", simulator.testPlan(identity).rows().get(1).stepName());
-            assertEquals("进行中", simulator.testPlan(identity).rows().get(1).executionStatus());
-        }
-    }
-
-    @Test
-    void oldChannelStateReceivesDefaultConfigurationWithoutResettingIdentity()
-    {
-        var object = JSON.parseObject(JSON.toJSONString(state(7, "127.0.0.9")));
-        object.getJSONArray("devices").getJSONObject(0).getJSONArray("channels").getJSONObject(0).remove("configuration");
-        var restored = store(new AtomicReference<>(object.toJSONString())).loadOrInitialize(state(1, "127.0.0.2"));
-        assertEquals(7, restored.devices().get(0).deviceId());
-        assertEquals("127.0.0.9", restored.devices().get(0).ip());
-        assertEquals(1, restored.devices().get(0).channels().get(0).configuration().steps().size());
-    }
-
-    @Test
-    void initializesOnlyWhenStateIsAbsentAndRestoresSavedState()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>();
-        RedisBtsSimulatorStateStore store = store(stored);
-        SimulatorState defaults = state(1, "127.0.0.2");
-
-        assertEquals(defaults, store.loadOrInitialize(defaults));
-        SimulatorState changed = state(7, "127.0.0.9");
-        store.save(changed);
-
-        assertEquals(changed, store.loadOrInitialize(defaults));
-    }
-
-    @Test
-    void restoresNestedSimulatorStateFromRedisSerializer()
-    {
-        SimulatorState expected = new SimulatorState(1,
-                List.of(new DeviceState(2, "127.0.0.3", false,
-                        List.of(new ChannelState(1, true, DeviceWorkStatus.WORKING)))),
-                List.of(new CollectorState(3, "dev-sim-collector-3", "127.0.1.3",
-                        "02:43:00:00:00:03", "模拟采集器", "开发模拟器",
-                        "Virtual Collector", 3, true)),
-                3, 4, List.of(new ChannelTombstone(1, "127.0.0.2", 4, 2)), false, 900L);
-        FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
-        AtomicReference<Object> stored = new AtomicReference<>(
-                serializer.deserialize(serializer.serialize(JSON.toJSONString(expected))));
-
-        SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2"));
-
-        assertEquals(expected, restored);
-        assertEquals(DeviceWorkStatus.WORKING, restored.devices().get(0).channels().get(0).workStatus());
-    }
-
-    @Test
-    void restoresChannelPlanWhenRedisReturnsNestedJsonObjects()
-    {
-        JSONObject state = JSON.parseObject(JSON.toJSONString(state(1, "127.0.0.2")));
-        JSONObject configuration = state.getJSONArray("devices").getJSONObject(0)
-                .getJSONArray("channels").getJSONObject(0).getJSONObject("configuration");
-        configuration.getJSONArray("steps").forEach(item -> ((JSONObject) item).put("@type", JSONObject.class.getName()));
-        FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
-        Object restoredValue = serializer.deserialize(serializer.serialize(state));
-
-        SimulatorState restored = store(new AtomicReference<>(restoredValue))
-                .loadOrInitialize(state(9, "127.0.0.9"));
-
-        assertEquals("恒流充电", restored.devices().get(0).channels().get(0)
-                .configuration().steps().get(0).name());
-    }
-
-    @Test
-    void upgradesLegacyCollectorDiscoveryStateWithSamplingDefaults()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>("{\"version\":1,\"devices\":[],"
-                + "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\","
-                + "\"ip\":\"127.0.1.2\",\"mac\":\"02:43:00:00:00:01\","
-                + "\"name\":\"模拟采集器\",\"manufacturer\":\"开发模拟器\","
-                + "\"model\":\"Virtual Collector\",\"channelCount\":2,\"online\":true}],"
-                + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
-
-        SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2"));
-
-        CollectorState collector = restored.collectors().get(0);
-        assertEquals(1000, collector.sampleIntervalMs());
-        assertEquals(List.of("S-01", "S-02"),
-                collector.channels().stream().map(channel -> channel.channelCode()).toList());
-    }
-
-    @Test
-    void rejectsIncompatibleVersionWithoutOverwritingIt()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>("{\"version\":2,\"devices\":[],"
-                + "\"collectors\":[],\"nextDeviceId\":1,\"nextCollectorId\":1,\"tombstones\":[]}");
-        RedisBtsSimulatorStateStore store = store(stored);
-
-        IllegalStateException exception = assertThrows(IllegalStateException.class,
-                () -> store.loadOrInitialize(state(1, "127.0.0.2")));
-
-        assertEquals("Unsupported BTS simulator state version: 2", exception.getMessage());
-        assertEquals(true, stored.get().toString().contains("\"version\":2"));
-    }
-
-    @Test
-    void restoresNestedDiscoveryCandidatesFromJson()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>();
-        RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored));
-        java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
-        CabinetCandidate candidate = new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
-                "127.0.0.2", null, "模拟柜", "开发模拟器", "BTS Virtual Cabinet",
-                List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
-                        DeviceWorkStatus.WORKING, observedAt)), observedAt, observedAt.plusSeconds(30));
-        first.recordCabinets(List.of(candidate));
-
-        RedisDeviceDiscoveryCandidateStore restored = new RedisDeviceDiscoveryCandidateStore(redis(stored));
-        CabinetCandidate actual = restored.snapshot(observedAt.plusSeconds(1)).cabinets().get(0);
-
-        assertEquals("dev-sim-22-1", actual.deviceCode());
-        assertEquals(DeviceWorkStatus.WORKING, actual.channels().get(0).workStatus());
-        assertEquals(observedAt, actual.channels().get(0).observedAt());
-    }
-
-    @Test
-    void removesDevelopmentCandidatesFromRedisState()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>();
-        RedisDeviceDiscoveryCandidateStore store = new RedisDeviceDiscoveryCandidateStore(redis(stored));
-        java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
-        store.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
-                "127.0.0.2", null, "模拟柜", null, null,
-                List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
-                        DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30))));
-
-        store.removeAdapter("dev-sim");
-
-        assertEquals(List.of(), store.snapshot(observedAt).cabinets());
-    }
-
-    @Test
-    void restoresDiscoveryCandidatesFromLegacyAutoTypeJson()
-    {
-        AtomicReference<Object> stored = new AtomicReference<>();
-        RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored));
-        java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
-        first.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
-                "127.0.0.2", null, "模拟柜", null, null,
-                List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
-                        DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30))));
-        first.replaceCollectors("dev-sim", List.of(new CollectorCandidate("dev-sim", "dev-sim-collector-1",
-                "127.0.1.2", null, "模拟采集器", null, null, 8, true,
-                observedAt, observedAt.plusSeconds(30))));
-
-        JSONObject legacy = JSON.parseObject(stored.get().toString());
-        legacy.put("@type", "com.battery.business.runtime.RedisDeviceDiscoveryCandidateStore$StoredState");
-        JSONObject cabinet = legacy.getJSONArray("cabinets").getJSONObject(0);
-        cabinet.put("@type", JSONObject.class.getName());
-        cabinet.getJSONArray("channels").getJSONObject(0)
-                .put("@type", JSONObject.class.getName());
-        legacy.getJSONArray("collectors").getJSONObject(0)
-                .put("@type", JSONObject.class.getName());
-        FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
-        stored.set(serializer.deserialize(serializer.serialize(legacy)));
-
-        var snapshot = new RedisDeviceDiscoveryCandidateStore(redis(stored)).snapshot(observedAt.plusSeconds(1));
-        CabinetCandidate restored = snapshot.cabinets().get(0);
-
-        assertEquals("dev-sim-22-1", restored.deviceCode());
-        assertEquals(DeviceWorkStatus.STOPPED, restored.channels().get(0).workStatus());
-        assertEquals("dev-sim-collector-1", snapshot.collectors().get(0).deviceCode());
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    private RedisBtsSimulatorStateStore store(AtomicReference<Object> stored)
-    {
-        return new RedisBtsSimulatorStateStore(redis(stored));
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    private RedisTemplate<Object, Object> redis(AtomicReference<Object> stored)
-    {
-        RedisTemplate<Object, Object> redis = mock(RedisTemplate.class);
-        ValueOperations<Object, Object> values = mock(ValueOperations.class);
-        ListOperations<Object, Object> lists = mock(ListOperations.class);
-        SetOperations<Object, Object> sets = mock(SetOperations.class);
-        Map<Object, List<Object>> listValues = new LinkedHashMap<>();
-        when(redis.opsForValue()).thenReturn(values);
-        when(redis.opsForList()).thenReturn(lists);
-        when(redis.opsForSet()).thenReturn(sets);
-        when(sets.add(any(), any())).thenReturn(1L);
-        when(sets.members(any())).thenReturn(Set.of());
-        when(redis.exec()).thenReturn(List.of(Boolean.TRUE));
-        when(redis.execute(any(SessionCallback.class))).thenAnswer(invocation -> {
-            SessionCallback<?> callback = invocation.getArgument(0);
-            return callback.execute(redis);
-        });
-        when(lists.rightPushAll(any(), org.mockito.ArgumentMatchers.<Object>anyCollection()))
-                .thenAnswer(invocation -> {
-                    Object key = invocation.getArgument(0);
-                    List<Object> records = new ArrayList<>(listValues.getOrDefault(key, List.of()));
-                    FastJson2JsonRedisSerializer<Object> serializer =
-                            new FastJson2JsonRedisSerializer<>(Object.class);
-                    List<Object> appended = invocation.getArgument(1);
-                    appended.forEach(item -> records.add(
-                            serializer.deserialize(serializer.serialize(item))));
-                    listValues.put(key, records);
-                    return (long) records.size();
-                });
-        when(lists.range(any(), anyLong(), anyLong())).thenAnswer(invocation -> {
-            List<Object> records = listValues.getOrDefault(invocation.getArgument(0), List.of());
-            long requestedStart = invocation.getArgument(1);
-            long requestedEnd = invocation.getArgument(2);
-            int start = (int) Math.min(requestedStart, records.size());
-            int end = (int) Math.min(requestedEnd + 1, records.size());
-            return start >= end ? List.of() : List.copyOf(records.subList(start, end));
-        });
-        when(lists.size(any())).thenAnswer(invocation ->
-                (long) listValues.getOrDefault(invocation.getArgument(0), List.of()).size());
-        when(redis.delete(org.mockito.ArgumentMatchers.<Object>any()))
-                .thenAnswer(invocation -> listValues.remove(invocation.getArgument(0)) != null);
-        when(values.get(any())).thenAnswer(invocation -> stored.get());
-        when(values.setIfAbsent(any(), any())).thenAnswer(invocation -> {
-            if (stored.get() != null) return false;
-            stored.set(invocation.getArgument(1));
-            return true;
-        });
-        doAnswer(invocation -> { stored.set(invocation.getArgument(1)); return null; })
-                .when(values).set(any(), any());
-        return redis;
-    }
-
-    private SimulatorState state(int deviceId, String ip)
-    {
-        return new SimulatorState(1,
-                List.of(new DeviceState(deviceId, ip, true,
-                        List.of(new ChannelState(1, true, DeviceWorkStatus.STOPPED)))),
-                List.of(), deviceId + 1, 1, List.of());
-    }
-}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/runtime/VendorSourceInstanceMigrationContractTest.java b/ruoyi-admin/src/test/java/com/battery/business/runtime/VendorSourceInstanceMigrationContractTest.java
index 643bb07..ce0ed9b 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/runtime/VendorSourceInstanceMigrationContractTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/runtime/VendorSourceInstanceMigrationContractTest.java
@@ -52,8 +52,8 @@
                     "neware76", "{\"control\":false}", "{\"database\":\"bts63\",\"user\":\"visitor\"}");
             assertEquals(1, jdbc.queryForObject(
                     "select count(*) from t_device_adapter where source_config is not null", Integer.class));
-            // 代码装配实例(例如开发模拟器)保持为空。
-            jdbc.update("insert into t_device_adapter(adapter_code, capabilities) values(?,?)", "dev-sim",
+            // 没有来源形态配置的行(列可空)仍允许存在。
+            jdbc.update("insert into t_device_adapter(adapter_code, capabilities) values(?,?)", "legacy-row",
                     "{\"control\":false}");
             assertEquals(1, jdbc.queryForObject(
                     "select count(*) from t_device_adapter where source_config is null", Integer.class));
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/CollectorDiscoveryServiceImplTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/CollectorDiscoveryServiceImplTest.java
deleted file mode 100644
index c42ac66..0000000
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/CollectorDiscoveryServiceImplTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.battery.business.service.impl;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import java.util.List;
-import java.util.Set;
-import org.junit.jupiter.api.Test;
-import com.battery.business.runtime.DevelopmentCollectorDiscoveryService;
-import com.battery.business.runtime.DevelopmentCollectorStatusSynchronizer;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.business.domain.vo.DiscoveredCollectorVo;
-import com.battery.business.mapper.TCollectorMapper;
-import com.battery.business.service.ITCollectorService;
-
-/**
- * 采集器设备发现服务测试。
- */
-public class CollectorDiscoveryServiceImplTest
-{
-    @Test
-    public void shouldReturnCollectorsWithoutMixingCabinets() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            ITCollectorService collectorService = mock(ITCollectorService.class);
-            when(collectorService.selectRegisteredCollectorCodes()).thenReturn(Set.of());
-            InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-            DevelopmentCollectorDiscoveryService service =
-                    new DevelopmentCollectorDiscoveryService(store, collectorService);
-            TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
-            simulator.addDevice("127.0.0.8", 2);
-            simulator.addCollector("127.0.1.3", 6);
-            new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper).synchronizeOnlineStatus();
-
-            List<DiscoveredCollectorVo> result = service.selectDiscoveredCollectorList();
-
-            assertEquals(2, result.size());
-            DiscoveredCollectorVo first = result.get(0);
-            assertEquals("dev-sim-collector-1", first.getDeviceCode());
-            assertEquals("127.0.1.2", first.getIp());
-            assertEquals("02:43:00:00:00:01", first.getMac());
-            assertEquals("dev-sim 采集器 1", first.getName());
-            assertEquals("开发模拟器", first.getManufacturer());
-            assertEquals("Virtual Collector", first.getModel());
-            assertEquals(4, first.getChannelCount());
-            assertEquals("在线", first.getOnlineStatus());
-        }
-    }
-
-    @Test
-    public void shouldReturnOfflineUnregisteredCollectors() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            ITCollectorService collectorService = mock(ITCollectorService.class);
-            when(collectorService.selectRegisteredCollectorCodes()).thenReturn(Set.of());
-            InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-            DevelopmentCollectorDiscoveryService service =
-                    new DevelopmentCollectorDiscoveryService(store, collectorService);
-            TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
-            simulator.updateCollector(1, "127.0.1.9", 8, false);
-            new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper).synchronizeOnlineStatus();
-
-            List<DiscoveredCollectorVo> result = service.selectDiscoveredCollectorList();
-
-            assertEquals(1, result.size());
-            assertEquals("dev-sim-collector-1", result.get(0).getDeviceCode());
-            assertEquals("离线", result.get(0).getOnlineStatus());
-        }
-    }
-
-    @Test
-    public void shouldExcludeRegisteredCollectorAndReturnItAfterDeletion() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            ITCollectorService collectorService = mock(ITCollectorService.class);
-            InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
-            DevelopmentCollectorDiscoveryService service =
-                    new DevelopmentCollectorDiscoveryService(store, collectorService);
-            TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
-            simulator.updateCollector(1, "127.0.1.2", 4, false);
-            new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper).synchronizeOnlineStatus();
-            when(collectorService.selectRegisteredCollectorCodes())
-                    .thenReturn(Set.of("dev-sim-collector-1"))
-                    .thenReturn(Set.of());
-
-            assertEquals(0, service.selectDiscoveredCollectorList().size());
-            assertEquals(1, service.selectDiscoveredCollectorList().size());
-        }
-    }
-}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceServiceTest.java
index a68bc03..09e335e 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceServiceTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceServiceTest.java
@@ -19,34 +19,45 @@
 
 class DeviceDiscoveryPersistenceServiceTest
 {
+    /**
+     * 来源不可达时,既有候选保留来源身份但旧快照被撤下、在线转为离线,登记设备同步离线;
+     * 来源恢复后下一次成功发现重新带回实时事实。
+     */
     @Test
-    void failedDiscoveryInvalidatesSuccessfulSnapshotUntilSoftwareReturns() throws Exception
+    void failedDiscoveryInvalidatesSuccessfulSnapshotUntilSourceReturns()
     {
-        try (var simulator = new com.battery.device.simulator.BtsDevelopmentSimulator(0);
-             var registry = new DeviceAdapterRegistry(List.of(new com.battery.device.bts.BtsDeviceAdapterFactory())))
-        {
-            registry.synchronize(new com.battery.device.api.registry.AdapterInstanceConfig("dev-sim", "BTS",
-                    "127.0.0.1", simulator.port(), 5000, "Asia/Hong_Kong", true,
-                    com.battery.device.api.model.AdapterCapabilities.observeOnly()));
-            var store = new InMemoryDeviceDiscoveryCandidateStore();
-            var mapper = Mockito.mock(TDetectionCabinetMapper.class);
-            var persistence = new DeviceDiscoveryPersistenceService(store, mapper);
-            var scheduler = new com.battery.business.runtime.DevelopmentSimulatorDiscoveryScheduler(registry, persistence);
-            simulator.updateChannel(1, 1, true, com.battery.device.api.model.DeviceWorkStatus.WORKING);
-            scheduler.discover();
-            var before = store.snapshot(java.time.Instant.now()).cabinets().get(0);
-            org.junit.jupiter.api.Assertions.assertNotNull(before.channels().get(0).latestSnapshot());
-            simulator.setSoftwareAccessible(false);
-            scheduler.discover();
-            var failed = store.snapshot(java.time.Instant.now()).cabinets().get(0);
-            assertEquals(before.deviceCode(), failed.deviceCode());
-            org.junit.jupiter.api.Assertions.assertFalse(failed.online());
-            org.junit.jupiter.api.Assertions.assertNull(failed.channels().get(0).latestSnapshot());
-            verify(mapper).markRegisteredCabinetsOffline("dev-sim");
-            simulator.setSoftwareAccessible(true);
-            scheduler.discover();
-            org.junit.jupiter.api.Assertions.assertTrue(store.snapshot(java.time.Instant.now()).cabinets().get(0).online());
-        }
+        InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
+        TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class);
+        DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class);
+        DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class);
+        ChannelIdentity identity = new ChannelIdentity("neware76", 21, 36, 1, 2);
+        java.time.Instant observedAt = java.time.Instant.parse("2026-09-28T01:54:56Z");
+        com.battery.device.api.model.RealtimeSnapshot snapshot = new com.battery.device.api.model.RealtimeSnapshot(
+                identity, com.battery.device.api.model.DeviceWorkStatus.WORKING, 3, 7, "1", null,
+                new java.math.BigDecimal("1.2"), new java.math.BigDecimal("3.3"),
+                new java.math.BigDecimal("0.5"), new java.math.BigDecimal("1.1"), 60L, 30L, null, observedAt, java.util.Map.of());
+        when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "26.1.2.3", true)));
+        when(adapter.inquire(any())).thenReturn(java.util.Map.of(identity, snapshot));
+        when(registry.execute(eq("neware76"), any())).thenAnswer(invocation -> {
+            @SuppressWarnings("unchecked")
+            Function<DeviceAdapter, Object> operation = (Function<DeviceAdapter, Object>) invocation.getArgument(1);
+            return operation.apply(adapter);
+        });
+        DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(store, cabinetMapper);
+        service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource(
+                "neware76", 8L, "新威 7.6 那台", 60000, true));
+
+        var before = store.snapshot(java.time.Instant.now()).cabinets().get(0);
+        org.junit.jupiter.api.Assertions.assertNotNull(before.channels().get(0).latestSnapshot());
+        org.junit.jupiter.api.Assertions.assertTrue(before.online());
+
+        service.markUnavailable("neware76");
+
+        var failed = store.snapshot(java.time.Instant.now()).cabinets().get(0);
+        assertEquals(before.deviceCode(), failed.deviceCode());
+        org.junit.jupiter.api.Assertions.assertFalse(failed.online());
+        org.junit.jupiter.api.Assertions.assertNull(failed.channels().get(0).latestSnapshot());
+        verify(cabinetMapper).markRegisteredCabinetsOffline("neware76");
     }
 
     @Test
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/RoutingAdapterWiringContractTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/RoutingAdapterWiringContractTest.java
index 3b36194..5bee422 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/RoutingAdapterWiringContractTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/RoutingAdapterWiringContractTest.java
@@ -8,8 +8,8 @@
 import com.battery.device.api.TestDataObservationAdapter;
 
 /**
- * 取数入口的装配契约:开发模拟器既是 Spring Bean,又实现了测试数据观测契约,
- * 所以业务侧注入的那个 Bean 必须显式声明为首选,否则上下文里出现两个候选会启动失败。
+ * 取数入口的装配契约:业务侧只注入一个测试数据观测 Bean,各来源实例由适配器注册表在运行时提供,
+ * 所以这个入口必须显式声明为首选,否则上下文里出现两个候选会启动失败。
  * 这里断言的是"哪个 Bean 会被注入"这个结构性事实,而不是 Spring 内部行为。
  */
 class RoutingAdapterWiringContractTest
@@ -22,12 +22,5 @@
                 "取数入口必须是可注入的 Bean");
         assertTrue(RoutingTestDataObservationAdapter.class.isAnnotationPresent(Primary.class),
                 "来源实例自己也实现该契约时必须显式首选路由入口,否则注入出现两个候选");
-    }
-
-    @Test
-    void developmentSimulatorAlsoExposesTheContractWhichIsWhyPreferenceIsNeeded()
-    {
-        assertTrue(TestDataObservationAdapter.class
-                .isAssignableFrom(com.battery.device.simulator.BtsDevelopmentSimulator.class));
     }
 }
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
index 3aef6e8..f132f1f 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
@@ -3,6 +3,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -24,12 +25,15 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import com.ruoyi.common.exception.ServiceException;
 import com.battery.business.domain.TCollector;
+import com.battery.business.domain.TDeviceAdapter;
 import com.battery.business.mapper.TCollectorMapper;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
+import com.battery.business.mapper.TDeviceAdapterMapper;
 
 /**
  * 采集器 Service 单元测试。
+ *
+ * <p>采集器按 ADR-0064 不建独立设备模型:登记不再依赖发现候选(真实来源不产出采集器候选),
+ * 由操作员手工登记来源身份,在线状态取所链接来源实例的连接结果。
  */
 @ExtendWith(MockitoExtension.class)
 class TCollectorServiceImplTest
@@ -37,20 +41,17 @@
     @Mock
     private TCollectorMapper collectorMapper;
 
+    @Mock
+    private TDeviceAdapterMapper adapterMapper;
+
     private TCollectorServiceImpl collectorService;
-    private InMemoryDeviceDiscoveryCandidateStore candidateStore;
 
     private TCollector collector;
 
     @BeforeEach
     void setUp()
     {
-        candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
-        collectorService = new TCollectorServiceImpl(collectorMapper, candidateStore);
-        java.time.Instant now = java.time.Instant.now();
-        candidateStore.replaceCollectors("dev-sim", java.util.List.of(new CollectorCandidate(
-                "dev-sim", "COL-001", "192.168.1.10", "AA:BB:CC:DD:EE:FF", "一号采集器",
-                "开发模拟器", "Virtual Collector", 8, true, now, now.plusSeconds(60))));
+        collectorService = new TCollectorServiceImpl(collectorMapper, adapterMapper);
         collector = new TCollector();
         collector.setCollectorId(1L);
         collector.setCollectorCode("COL-001");
@@ -80,21 +81,52 @@
         }
     }
 
+    /** 在线事实只有一个来源:所链接来源实例的连接结果;登记表里的存储列不再充当这个事实。 */
     @Test
-    void registeredCollectorCodeSqlShouldIgnoreDeletedRows() throws IOException
+    void onlineStatusSqlShouldDeriveFromLinkedAdapter() throws IOException
     {
         try (InputStream input = getClass().getResourceAsStream(
                 "/mapper/battery/TCollectorMapper.xml"))
         {
             assertNotNull(input);
             String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
-            int selectStart = mapperXml.indexOf("<select id=\"selectRegisteredCollectorCodes\"");
-            assertTrue(selectStart >= 0);
-            int selectEnd = mapperXml.indexOf("</select>", selectStart);
-            String selectSql = mapperXml.substring(selectStart, selectEnd);
-
-            assertTrue(selectSql.contains("where del_flag = '0'"));
+            int fragmentStart = mapperXml.indexOf("<sql id=\"derivedOnlineStatus\"");
+            assertTrue(fragmentStart >= 0, "在线状态的派生表达式应当是一个共用片段");
+            String fragment = mapperXml.substring(fragmentStart, mapperXml.indexOf("</sql>", fragmentStart));
+            assertTrue(fragment.contains("from t_device_adapter adapter_fact"), fragment);
+            // 存储列不再参与:只有派生的那个表达式。
+            assertFalse(mapperXml.contains("upper(c.online_status)"));
+            // SELECT 与 WHERE 各用一次(WHERE 里不能用别名,所以走同一个片段)。
+            assertEquals(2, mapperXml.split("<include refid=\"derivedOnlineStatus\" />", -1).length - 1);
         }
+    }
+
+    /** 登记不再要求发现候选:手工填写的来源身份即可落库。 */
+    @Test
+    void insertCollectorShouldNotRequireADiscoveryCandidate()
+    {
+        collector.setCollectorId(null);
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        verify(collectorMapper).insertCollector(any(TCollector.class));
+    }
+
+    @Test
+    void insertCollectorShouldAcceptCollectorWithoutIpAndMac()
+    {
+        collector.setCollectorId(null);
+        collector.setIpAddress(null);
+        collector.setMacAddress(null);
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).insertCollector(captor.capture());
+        assertNull(captor.getValue().getIpAddress());
+        assertNull(captor.getValue().getMacAddress());
     }
 
     @Test
@@ -117,6 +149,55 @@
         assertEquals("0", saved.getTestStatus());
         assertEquals("0", saved.getStatus());
         assertEquals("0", saved.getDelFlag());
+    }
+
+    @Test
+    void insertCollectorShouldRejectMalformedIpAndMacInsteadOfGuessing()
+    {
+        collector.setCollectorId(null);
+        collector.setIpAddress("invalid-host");
+        collector.setMacAddress("AA:BB:CC:DD:EE:FF");
+
+        ServiceException ip = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+        assertEquals("IP地址格式不正确", ip.getMessage());
+
+        collector.setIpAddress("192.168.1.10");
+        collector.setMacAddress("invalid-mac");
+        ServiceException mac = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+        assertEquals("MAC地址格式不正确", mac.getMessage());
+        verify(collectorMapper, never()).insertCollector(any(TCollector.class));
+    }
+
+    @Test
+    void insertCollectorShouldRejectUnknownLinkedSource()
+    {
+        collector.setCollectorId(null);
+        collector.setAdapterId(9L);
+        when(adapterMapper.selectDeviceAdapterById(9L)).thenReturn(null);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+
+        assertEquals("所链接的数据来源不存在", exception.getMessage());
+        verify(collectorMapper, never()).insertCollector(any(TCollector.class));
+    }
+
+    /** 链接到来源实例后,登记保存该链接:采集器的辅助测量由它提供。 */
+    @Test
+    void insertCollectorShouldPersistLinkedSource()
+    {
+        collector.setCollectorId(null);
+        collector.setAdapterId(3L);
+        when(adapterMapper.selectDeviceAdapterById(3L)).thenReturn(new TDeviceAdapter());
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).insertCollector(captor.capture());
+        assertEquals(Long.valueOf(3L), captor.getValue().getAdapterId());
     }
 
     @Test
@@ -148,36 +229,6 @@
     }
 
     @Test
-    void insertCollectorShouldValidateAuthoritativeCandidateTopology()
-    {
-        collector.setCollectorId(null);
-        collector.setIpAddress("invalid-host");
-        collector.setMacAddress("invalid-mac");
-        collector.setChannelCount(99);
-        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
-
-        assertEquals(1, collectorService.insertCollector(collector));
-
-        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
-        verify(collectorMapper).insertCollector(captor.capture());
-        assertEquals("192.168.1.10", captor.getValue().getIpAddress());
-        assertEquals("AA:BB:CC:DD:EE:FF", captor.getValue().getMacAddress());
-        assertEquals(8, captor.getValue().getChannelCount());
-    }
-
-    @Test
-    void offlineCollectorCanBeRegisteredWithoutChangingOnlineFact()
-    {
-        java.time.Instant now = java.time.Instant.now();
-        candidateStore.replaceCollectors("dev-sim", java.util.List.of(new CollectorCandidate(
-                "dev-sim", "COL-001", "192.168.1.10", "AA:BB:CC:DD:EE:FF", "一号采集器",
-                "开发模拟器", "Virtual Collector", 8, false, now, now.plusSeconds(60))));
-        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
-        assertEquals(1, collectorService.insertCollector(collector));
-        verify(collectorMapper).updateRegisteredObservation("COL-001", "OFFLINE");
-    }
-
-    @Test
     void insertCollectorShouldRejectDuplicateCode()
     {
         collector.setCollectorId(null);
@@ -203,6 +254,27 @@
         assertEquals(1, collectorService.updateCollector(collector));
 
         verify(collectorMapper).updateCollector(collector);
+    }
+
+    /** 编辑只提交可改字段:未提交的 IP/MAC 不能被当成"清空"。 */
+    @Test
+    void updateCollectorShouldKeepUnsubmittedIpAndMac()
+    {
+        when(collectorMapper.selectCollectorById(1L)).thenReturn(collector);
+        when(collectorMapper.selectCollectorByCode("COL-001")).thenReturn(collector);
+        when(collectorMapper.selectCollectorByMacAddress("AA:BB:CC:DD:EE:FF")).thenReturn(collector);
+        when(collectorMapper.updateCollector(any(TCollector.class))).thenReturn(1);
+        TCollector edit = new TCollector();
+        edit.setCollectorId(1L);
+        edit.setCollectorName("改名后");
+
+        assertEquals(1, collectorService.updateCollector(edit));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).updateCollector(captor.capture());
+        assertEquals("192.168.1.10", captor.getValue().getIpAddress());
+        assertEquals("AA:BB:CC:DD:EE:FF", captor.getValue().getMacAddress());
+        assertEquals("改名后", captor.getValue().getCollectorName());
     }
 
     @Test
@@ -256,14 +328,6 @@
         assertEquals(1, collectorService.updateCollectorStatus(command));
 
         verify(collectorMapper, never()).updateCollectorStatus(any(TCollector.class));
-    }
-
-    @Test
-    void selectRegisteredCollectorCodesShouldReturnUndeletedCodes()
-    {
-        when(collectorMapper.selectRegisteredCollectorCodes()).thenReturn(java.util.List.of("COL-001"));
-
-        assertEquals(java.util.Set.of("COL-001"), collectorService.selectRegisteredCollectorCodes());
     }
 
     @Test
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TTestLiveServiceImplTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TTestLiveServiceImplTest.java
index c216676..3178319 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TTestLiveServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TTestLiveServiceImplTest.java
@@ -61,7 +61,7 @@
         var mapper = mock(TTestMapper.class); when(mapper.selectCollectorCode(7L)).thenReturn("collector-7");
         var adapter = mock(TestDataObservationAdapter.class);
         var service = new TTestLiveServiceImpl(tests,
-                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, mapper);
+                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, mapper, null);
         test.setReceptionBoundaryAtUtc(java.util.Date.from(now));
         for (Instant deviceTime : List.of(now.minusSeconds(1), now)) {
             var measurement = new TestDataObservationAdapter.CollectorMeasurement("S-01", 1L, deviceTime, deviceTime.toString(),
@@ -77,68 +77,74 @@
     @Test
     void currentTemperatureAlarmIsIndependentOfHistoricalTodoHandling() throws Exception
     {
-        try (var simulator = new com.battery.device.simulator.BtsDevelopmentSimulator(0)) {
-            TTest test = test("RECEIVING");
-            var battery = test.getBatteries().get(0); battery.setCollectorId(7L);
-            var primary = new com.battery.business.domain.TTestCollectorChannel();
-            primary.setChannelCode("S-01"); primary.setMeasurementCode("PRIMARY_TEMPERATURE");
-            var secondary = new com.battery.business.domain.TTestCollectorChannel();
-            secondary.setChannelCode("S-03"); secondary.setMeasurementCode("SECONDARY_TEMPERATURE_2");
-            battery.setCollectorChannels(List.of(primary, secondary));
-            var rule = new com.battery.business.domain.TTestWarningThreshold();
-            rule.setThresholdId(11L); rule.setMetricCode("TEMPERATURE");
-            rule.setMinValue(new BigDecimal("8")); rule.setMaxValue(new BigDecimal("33"));
-            test.setThresholds(List.of(rule));
-            var tests = mock(ITTestService.class); when(tests.selectTestById(10L)).thenReturn(test);
-            var mapper = mock(TTestMapper.class); when(mapper.selectCollectorCode(7L)).thenReturn("dev-sim-collector-1");
-            var service = new TTestLiveServiceImpl(tests,
-                    new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), simulator, mapper);
-            simulator.configureCollectorChannel(1, "S-01", List.of(
-                    new com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState("temperature", new BigDecimal("38"), "℃")));
-            var alarms = service.current(10L).exceptions().stream().filter(item -> "THRESHOLD".equals(item.get("exceptionType"))).toList();
-            assertEquals(1, alarms.size());
-            assertEquals(new BigDecimal("38"), alarms.get(0).get("value"));
-            assertEquals("7:S-01", alarms.get(0).get("sourceIdentity"));
-            assertFalse(alarms.get(0).containsKey("exceptionId"));
+        TTest test = test("RECEIVING");
+        var battery = test.getBatteries().get(0); battery.setCollectorId(7L);
+        var primary = new com.battery.business.domain.TTestCollectorChannel();
+        primary.setChannelCode("S-01"); primary.setMeasurementCode("PRIMARY_TEMPERATURE");
+        var secondary = new com.battery.business.domain.TTestCollectorChannel();
+        secondary.setChannelCode("S-03"); secondary.setMeasurementCode("SECONDARY_TEMPERATURE_2");
+        battery.setCollectorChannels(List.of(primary, secondary));
+        var rule = new com.battery.business.domain.TTestWarningThreshold();
+        rule.setThresholdId(11L); rule.setMetricCode("TEMPERATURE");
+        rule.setMinValue(new BigDecimal("8")); rule.setMaxValue(new BigDecimal("33"));
+        test.setThresholds(List.of(rule));
+        var tests = mock(ITTestService.class); when(tests.selectTestById(10L)).thenReturn(test);
+        var mapper = mock(TTestMapper.class); when(mapper.selectCollectorCode(7L)).thenReturn("dev-sim-collector-1");
+        var adapter = mock(TestDataObservationAdapter.class);
+        Instant now = Instant.now();
+        // 采集器来源交出主、辅助两路测量:只有绑定了的 S-01 主温度进预警。
+        when(adapter.latestCollector(org.mockito.ArgumentMatchers.nullable(String.class),
+                org.mockito.ArgumentMatchers.eq("dev-sim-collector-1"))).thenReturn(collectorRealtime(now, "38"));
+        var service = new TTestLiveServiceImpl(tests,
+                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, mapper, null);
+        var alarms = service.current(10L).exceptions().stream().filter(item -> "THRESHOLD".equals(item.get("exceptionType"))).toList();
+        assertEquals(1, alarms.size());
+        assertEquals(new BigDecimal("38"), alarms.get(0).get("value"));
+        assertEquals("7:S-01", alarms.get(0).get("sourceIdentity"));
+        assertFalse(alarms.get(0).containsKey("exceptionId"));
 
-            var history = new TTestWarningException(); history.setExceptionStatus("OPEN");
-            history.setExceptionType("THRESHOLD"); history.setMetricCode("temperature");
-            history.setAbnormalValue(new BigDecimal("38"));
-            when(mapper.selectWarningExceptions(10L, "OPEN")).thenReturn(List.of(history));
-            simulator.configureCollectorChannel(1, "S-01", List.of(
-                    new com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState("temperature", new BigDecimal("25"), "℃")));
-            assertTrue(service.current(10L).exceptions().stream().noneMatch(item -> "THRESHOLD".equals(item.get("exceptionType"))));
-            assertEquals("OPEN", history.getExceptionStatus());
-        }
+        var history = new TTestWarningException(); history.setExceptionStatus("OPEN");
+        history.setExceptionType("THRESHOLD"); history.setMetricCode("temperature");
+        history.setAbnormalValue(new BigDecimal("38"));
+        when(mapper.selectWarningExceptions(10L, "OPEN")).thenReturn(List.of(history));
+        when(adapter.latestCollector(org.mockito.ArgumentMatchers.nullable(String.class),
+                org.mockito.ArgumentMatchers.eq("dev-sim-collector-1"))).thenReturn(collectorRealtime(now, "25"));
+        assertTrue(service.current(10L).exceptions().stream().noneMatch(item -> "THRESHOLD".equals(item.get("exceptionType"))));
+        assertEquals("OPEN", history.getExceptionStatus());
+    }
+
+    private static TestDataObservationAdapter.CollectorRealtime collectorRealtime(Instant now, String temperature)
+    {
+        return new TestDataObservationAdapter.CollectorRealtime("dev-sim-collector-1", true, 1000,
+                List.of(new TestDataObservationAdapter.CollectorMeasurement("S-01", 1L, now, now.toString(), now,
+                        "TEMPERATURE", new BigDecimal(temperature), "℃")), null);
     }
 
     @Test
-    void consoleReadsConfiguredSimulatorPlanAtCurrentChannelPosition() throws Exception
+    void consoleReadsConfiguredPlanAtCurrentChannelPosition() throws Exception
     {
-        try (var simulator = new com.battery.device.simulator.BtsDevelopmentSimulator(0)) {
-            var test = test("RECEIVING");
-            var binding = test.getBatteries().get(0).getBinding();
-            binding.setDevtype(22); binding.setSubdevid(1);
-            simulator.configureChannel(1, 1, new com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration(
-                    BigDecimal.ONE, new BigDecimal("4.125"), BigDecimal.ONE, BigDecimal.ONE, 2, 2, 3, 120, 30,
-                    List.of(new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("充电", "cc", 60, 4.2),
-                            new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("放电", "dc", 90, 3.0))));
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
-            var tests = mock(ITTestService.class);
-            when(tests.selectTestById(10L)).thenReturn(test);
-            var service = new TTestLiveServiceImpl(tests,
-                    new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), simulator);
-            var plan = service.current(10L).testPlan();
-            assertEquals(2, plan.currentCycleNo());
-            assertEquals(2, plan.currentStepNo());
-            assertEquals(3, plan.plannedCycleCount());
-            assertEquals(List.of(1, 2), plan.rows().stream().map(row -> row.cyclePosition()).toList());
-            assertEquals("放电", plan.rows().get(1).stepName());
-            assertEquals(90d, plan.rows().get(1).durationSeconds());
-            assertEquals("进行中", plan.rows().get(1).executionStatus());
-            simulator.updateChannel(1, 1, false, DeviceWorkStatus.WORKING);
-            org.junit.jupiter.api.Assertions.assertNull(service.current(10L).testPlan());
-        }
+        var test = test("RECEIVING");
+        var binding = test.getBatteries().get(0).getBinding();
+        binding.setDevtype(22); binding.setSubdevid(1);
+        var tests = mock(ITTestService.class);
+        when(tests.selectTestById(10L)).thenReturn(test);
+        var adapter = mock(TestDataObservationAdapter.class);
+        // 来源交出它自己的方案:当前循环 2、当前工步 2、计划 3 个循环,工步行按循环位置排列。
+        when(adapter.testPlan(any(ChannelIdentity.class))).thenReturn(new TestDataObservationAdapter.TestPlan(2, 2, 3,
+                List.of(new TestDataObservationAdapter.PlanRow("STEP", 1, 1, "充电", 60, 4.2, "已完成", null, null),
+                        new TestDataObservationAdapter.PlanRow("STEP", 2, 2, "放电", 90, 3.0, "进行中", null, null))));
+        var service = new TTestLiveServiceImpl(tests,
+                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, null, null);
+        var plan = service.current(10L).testPlan();
+        assertEquals(2, plan.currentCycleNo());
+        assertEquals(2, plan.currentStepNo());
+        assertEquals(3, plan.plannedCycleCount());
+        assertEquals(List.of(1, 2), plan.rows().stream().map(row -> row.cyclePosition()).toList());
+        assertEquals("放电", plan.rows().get(1).stepName());
+        assertEquals(90d, plan.rows().get(1).durationSeconds());
+        assertEquals("进行中", plan.rows().get(1).executionStatus());
+        when(adapter.testPlan(any(ChannelIdentity.class))).thenReturn(null);
+        org.junit.jupiter.api.Assertions.assertNull(service.current(10L).testPlan());
     }
 
     @Test
@@ -212,36 +218,73 @@
         assertTrue(alarms.stream().anyMatch(item -> "CABINET_CHANNEL".equals(item.get("sourceType"))));
     }
 
+    /**
+     * 非接收状态只读来源可达性:状态取自所链接来源实例的连接结果,且**不请求**任何测量,
+     * 否则暂停期间仍会产生新观测。
+     */
     @Test
     void nonReceivingConsoleReadsCollectorStatusWithoutRequestingMeasurements() throws Exception
     {
-        try (var simulator = new com.battery.device.simulator.BtsDevelopmentSimulator(0))
+        TTest test = test("RECEIVING");
+        test.getBatteries().get(0).setCollectorId(7L);
+        var tests = mock(ITTestService.class);
+        when(tests.selectTestById(10L)).thenReturn(test);
+        var mapper = mock(TTestMapper.class);
+        when(mapper.selectCollectorCode(7L)).thenReturn("dev-sim-collector-1");
+        var adapter = mock(TestDataObservationAdapter.class);
+        var adapters = mock(com.battery.business.mapper.TDeviceAdapterMapper.class);
+        var source = new com.battery.business.domain.TDeviceAdapter();
+        source.setConnectionStatus("ONLINE");
+        when(adapters.selectDeviceAdapterByCode("dev-sim")).thenReturn(source);
+        var service = new TTestLiveServiceImpl(tests,
+                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, mapper, adapters);
+        test.setReceptionStatus("RECEIVING");
+        var receiving = service.current(10L).sources().stream()
+                .filter(item -> item.sourceType().equals("COLLECTOR")).findFirst().orElseThrow();
+        assertEquals("ONLINE", receiving.deviceStatus());
+        verify(adapter).latestCollector(org.mockito.ArgumentMatchers.eq("dev-sim"),
+                org.mockito.ArgumentMatchers.eq("dev-sim-collector-1"));
+        org.mockito.Mockito.clearInvocations(adapter);
+        for (String status : List.of("PAUSED", "ENDED", "NOT_STARTED"))
         {
-            TTest test = test("RECEIVING");
-            test.getBatteries().get(0).setCollectorId(7L);
-            var tests = mock(ITTestService.class);
-            when(tests.selectTestById(10L)).thenReturn(test);
-            var mapper = mock(TTestMapper.class);
-            when(mapper.selectCollectorCode(7L)).thenReturn("dev-sim-collector-1");
-            var candidates = new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore();
-            Instant now = Instant.now();
-            candidates.replaceCollectors("dev-sim", List.of(new DeviceDiscoveryCandidateStore.CollectorCandidate(
-                    "dev-sim", "dev-sim-collector-1", "127.0.1.2", null, "模拟采集器", null, null,
-                    4, true, now, now.plusSeconds(30))));
-            var service = new TTestLiveServiceImpl(tests, candidates, simulator, mapper);
-            service.current(10L);
-            var received = simulator.snapshot().recentRequests();
-            assertTrue(received.stream().anyMatch(item -> item.command().equals("collector-realtime")));
-            for (String status : List.of("PAUSED", "ENDED", "NOT_STARTED"))
-            {
-                test.setReceptionStatus(status);
-                var source = service.current(10L).sources().stream()
-                        .filter(item -> item.sourceType().equals("COLLECTOR")).findFirst().orElseThrow();
-                assertEquals("ONLINE", source.deviceStatus());
-                assertTrue(source.latestSnapshot().isEmpty());
-                assertEquals(received, simulator.snapshot().recentRequests());
-            }
+            test.setReceptionStatus(status);
+            var paused = service.current(10L).sources().stream()
+                    .filter(item -> item.sourceType().equals("COLLECTOR")).findFirst().orElseThrow();
+            assertEquals("ONLINE", paused.deviceStatus());
+            assertTrue(paused.latestSnapshot().isEmpty());
         }
+        verify(adapter, never()).latestCollector(org.mockito.ArgumentMatchers.anyString(),
+                org.mockito.ArgumentMatchers.anyString());
+    }
+
+    /** 来源不提供辅助测量(新威实测无辅助数据)时如实标注,并且不因此生成离线异常。 */
+    @Test
+    void collectorWithoutAuxiliaryMeasurementsStaysOnlineAndIsMarkedUnavailable() throws Exception
+    {
+        TTest test = test("RECEIVING");
+        test.getBatteries().get(0).setCollectorId(7L);
+        var tests = mock(ITTestService.class);
+        when(tests.selectTestById(10L)).thenReturn(test);
+        var mapper = mock(TTestMapper.class);
+        when(mapper.selectCollectorCode(7L)).thenReturn("neware76-collector");
+        var adapter = mock(TestDataObservationAdapter.class);
+        when(adapter.latestCollector(org.mockito.ArgumentMatchers.anyString(),
+                org.mockito.ArgumentMatchers.anyString())).thenReturn(null);
+        var adapters = mock(com.battery.business.mapper.TDeviceAdapterMapper.class);
+        var source = new com.battery.business.domain.TDeviceAdapter();
+        source.setConnectionStatus("ONLINE");
+        when(adapters.selectDeviceAdapterByCode("dev-sim")).thenReturn(source);
+        var service = new TTestLiveServiceImpl(tests,
+                new com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore(), adapter, mapper, adapters);
+
+        var collector = service.current(10L).sources().stream()
+                .filter(item -> item.sourceType().equals("COLLECTOR")).findFirst().orElseThrow();
+
+        assertEquals("ONLINE", collector.deviceStatus());
+        assertEquals(Boolean.FALSE, collector.latestSnapshot().get("auxiliaryDataAvailable"));
+        assertFalse(collector.latestSnapshot().containsKey("measurements"));
+        assertTrue(service.current(10L).exceptions().stream()
+                .noneMatch(item -> "COLLECTOR".equals(item.get("sourceType"))));
     }
 
     @Test
@@ -386,7 +429,7 @@
                 "dev-sim", 1L, "dev-sim", 1, 1, "127.0.0.2", "mac", "cabinet", "vendor", "model",
                 List.of(channel), now, now.plusSeconds(60));
         when(candidates.snapshot(any(Instant.class))).thenReturn(
-                new DeviceDiscoveryCandidateStore.CandidateSnapshot(List.of(cabinet), List.of()));
+                new DeviceDiscoveryCandidateStore.CandidateSnapshot(List.of(cabinet)));
         var plan = new TestDataObservationAdapter.TestPlan(1, 1, 1, List.of());
         when(simulator.testPlan(any(ChannelIdentity.class))).thenReturn(plan);
         if (mapper != null && test.getBatteries().get(0).getCollectorId() != null) {
@@ -464,7 +507,7 @@
         private CurrentLiveService(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
                 TestDataObservationAdapter simulator, TTestMapper mapper)
         {
-            super(tests, candidates, simulator, mapper);
+            super(tests, candidates, simulator, mapper, null);
         }
     }
 
diff --git a/ruoyi-admin/src/test/java/com/battery/business/web/device/BtsDevelopmentSimulatorControllerTest.java b/ruoyi-admin/src/test/java/com/battery/business/web/device/BtsDevelopmentSimulatorControllerTest.java
deleted file mode 100644
index aa11c90..0000000
--- a/ruoyi-admin/src/test/java/com/battery/business/web/device/BtsDevelopmentSimulatorControllerTest.java
+++ /dev/null
@@ -1,348 +0,0 @@
-package com.battery.business.web.device;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import java.lang.reflect.Method;
-import java.math.BigDecimal;
-import java.net.InetAddress;
-import java.time.Instant;
-import java.util.List;
-import org.junit.jupiter.api.Test;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.server.ResponseStatusException;
-import com.ruoyi.common.annotation.Anonymous;
-import com.battery.device.simulator.BtsDevelopmentSimulator;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.device.api.model.ChannelIdentity;
-import com.battery.device.api.model.DeviceWorkStatus;
-import com.battery.business.runtime.BtsDevelopmentSimulatorWebConfig;
-import com.battery.business.runtime.DevelopmentCollectorStatusSynchronizer;
-
-class BtsDevelopmentSimulatorControllerTest
-{
-    @Test
-    void invalidChannelConfigurationDoesNotReplaceSavedValues() throws Exception
-    {
-        try (var simulator = new BtsDevelopmentSimulator(0)) {
-            var mvc = org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller(simulator)).build();
-            var before = simulator.snapshot().devices().get(0).channels().get(0).configuration();
-            String valid = com.alibaba.fastjson2.JSON.toJSONString(before);
-            for (String property : List.of("stepNo", "cycleNo", "currentA", "steps")) {
-                var invalid = com.alibaba.fastjson2.JSON.parseObject(valid);
-                invalid.put(property, switch (property) {
-                    case "stepNo" -> 2;
-                    case "cycleNo" -> 0;
-                    case "steps" -> List.of();
-                    default -> null;
-                });
-                mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put(
-                        "/dev/bts-simulator/devices/1/channels/1/configuration")
-                        .contentType("application/json").content(invalid.toJSONString()))
-                        .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isBadRequest());
-                assertEquals(before, simulator.snapshot().devices().get(0).channels().get(0).configuration());
-            }
-        }
-    }
-
-    @Test
-    void channelConfigurationChangesMeasurementsAndPlanOnlyForSelectedChannel() throws Exception
-    {
-        try (var simulator = new BtsDevelopmentSimulator(0)) {
-            var mvc = org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller(simulator)).build();
-            mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put(
-                    "/dev/bts-simulator/devices/1/channels/1/configuration")
-                    .contentType("application/json").content("""
-                    {"currentA":-2.5,"voltageV":4.125,"capacityAh":8.2,"energyWh":33.8,
-                     "cycleNo":2,"stepNo":2,"plannedCycleCount":3,"totalTimeSeconds":120,"relativeTimeSeconds":30,
-                     "steps":[{"name":"充电","stepType":"cc","durationSeconds":60,"voltageV":4.2},
-                              {"name":"放电","stepType":"dc","durationSeconds":90,"voltageV":3.0}]}
-                    """))
-                    .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk());
-            var identity = simulator.discoverChannels().get(0).identity();
-            var actual = simulator.latestCabinet(identity);
-            assertEquals(-2.5, actual.currentA());
-            assertEquals(4.125, actual.voltageV());
-            assertEquals(8.2, actual.capacityAh());
-            assertEquals(33.8, actual.energyWh());
-            assertEquals(2, actual.cycleNo());
-            assertEquals(2, actual.stepNo());
-            assertEquals("dc", actual.stepType());
-            assertEquals("放电", actual.stepName());
-            assertEquals(2, simulator.testPlan(identity).rows().size());
-            assertEquals(3, simulator.testPlan(identity).plannedCycleCount());
-            var other = simulator.discoverChannels().get(1).identity();
-            assertEquals(1, simulator.testPlan(other).rows().size());
-            assertEquals(1, simulator.latestCabinet(other).stepNo());
-        }
-    }
-
-    @Test
-    void restChangesSoftwareAndCollectorChannelFactsAndRetiresPlayback() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            var mvc = org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller(simulator)).build();
-            mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put("/dev/bts-simulator/software")
-                    .contentType("application/json").content("{\"accessible\":false}"))
-                    .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk());
-            assertFalse(simulator.snapshot().softwareAccessible());
-            mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put(
-                    "/dev/bts-simulator/collectors/1/channels/S-01/status")
-                    .contentType("application/json").content("{\"workStatus\":\"PROTECTED\"}"))
-                    .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk());
-            assertEquals(DeviceWorkStatus.PROTECTED, simulator.snapshot().collectors().get(0).channels().get(0).workStatus());
-            for (String path : List.of("/devices/1/channels/1/playback/play", "/devices/1/channels/1/playback/next-step"))
-                mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/dev/bts-simulator" + path))
-                        .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isNotFound());
-            for (String path : List.of("/collectors/1/history", "/devices/1/channels/1/records"))
-                mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get("/dev/bts-simulator" + path))
-                        .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isNotFound());
-        }
-    }
-
-    @Test
-    void controllerUpdatesDeviceAndChannelState() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-
-            controller.updateDevice(1,
-                    new BtsDevelopmentSimulatorController.DeviceStateCommand("127.0.0.2", 4, false));
-            controller.updateChannel(1, 3,
-                    new BtsDevelopmentSimulatorController.ChannelStateCommand(true, DeviceWorkStatus.PAUSED));
-
-            assertFalse(controller.state().devices().get(0).online());
-            assertEquals(DeviceWorkStatus.PAUSED,
-                    controller.state().devices().get(0).channels().get(2).workStatus());
-        }
-    }
-
-    @Test
-    void controllerAddsUpdatesAndDeletesVirtualDevices() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-
-            var created = controller.createDevice(
-                    new BtsDevelopmentSimulatorController.CreateDeviceCommand("127.0.0.8", 6));
-            int deviceId = created.devices().stream()
-                    .filter(device -> device.ip().equals("127.0.0.8"))
-                    .findFirst().orElseThrow().deviceId();
-            controller.updateDevice(deviceId,
-                    new BtsDevelopmentSimulatorController.DeviceStateCommand("127.0.0.9", 3, false));
-
-            assertEquals(2, controller.state().devices().size());
-            assertEquals(3, controller.state().devices().stream()
-                    .filter(device -> device.deviceId() == deviceId)
-                    .findFirst().orElseThrow().channels().size());
-
-            controller.deleteDevice(deviceId);
-            assertEquals(1, controller.state().devices().size());
-        }
-    }
-
-    @Test
-    void controllerCreatesNewareDeviceFromExplicitModelProfile() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-
-            var created = controller.createDevice(new BtsDevelopmentSimulatorController.CreateDeviceCommand(
-                    "127.0.0.8", 0, "CT-4008T-5V6A-S1-F"));
-            var device = created.devices().stream().filter(item -> item.ip().equals("127.0.0.8"))
-                    .findFirst().orElseThrow();
-
-            assertEquals(8, device.channels().size());
-            assertEquals("新威", device.profile().manufacturer());
-            assertEquals("CT-4008T-5V6A-S1-F", device.profile().model());
-            assertEquals(100, device.profile().sampleIntervalMs());
-        }
-    }
-
-    @Test
-    void controllerConfiguresCollectorsSeparatelyFromCabinets() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-            var created = controller.createCollector(
-                    new BtsDevelopmentSimulatorController.CreateCollectorCommand("127.0.1.8", 6));
-            int collectorId = created.collectors().stream()
-                    .filter(collector -> collector.ip().equals("127.0.1.8"))
-                    .findFirst().orElseThrow().collectorId();
-
-            controller.updateCollector(collectorId,
-                    new BtsDevelopmentSimulatorController.CollectorStateCommand("127.0.1.9", 3, false));
-
-            assertEquals(1, controller.state().devices().size());
-            assertEquals(2, controller.state().collectors().size());
-            assertFalse(controller.state().collectors().stream()
-                    .filter(collector -> collector.collectorId() == collectorId)
-                    .findFirst().orElseThrow().online());
-
-            controller.deleteCollector(collectorId);
-            assertEquals(1, controller.state().collectors().size());
-            assertEquals(1, controller.state().devices().size());
-        }
-    }
-
-    @Test
-    void controllerExposesOnlyCurrentCollectorMeasurements() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-            controller.configureCollectorChannel(1, "S-01",
-                    new BtsDevelopmentSimulatorController.CollectorChannelCommand(List.of(
-                            new BtsDevelopmentSimulatorController.CollectorMetricCommand(
-                                    "temperature", new BigDecimal("27.2"), "℃"),
-                            new BtsDevelopmentSimulatorController.CollectorMetricCommand(
-                                    "custom_voltage", new BigDecimal("1.234"), "V"))));
-
-            var realtime = controller.collectorRealtime(1);
-
-            assertTrue(realtime.online());
-            assertTrue(realtime.measurements().stream().anyMatch(item -> item.metricCode().equals("custom_voltage")));
-
-        }
-    }
-
-    @Test
-    void controllerChangesOnlineFactsWithoutResettingChannelStatus() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-            simulator.updateChannel(1, 1, true, DeviceWorkStatus.PAUSED);
-
-            var poweredOff = controller.setPower(
-                    new BtsDevelopmentSimulatorController.SimulatorPowerCommand(false));
-
-            assertTrue(poweredOff.devices().stream().noneMatch(device -> device.online()));
-            assertTrue(poweredOff.collectors().stream().noneMatch(collector -> collector.online()));
-            assertEquals(DeviceWorkStatus.PAUSED, poweredOff.devices().get(0).channels().get(0).workStatus());
-
-            var poweredOn = controller.setPower(
-                    new BtsDevelopmentSimulatorController.SimulatorPowerCommand(true));
-            assertTrue(poweredOn.devices().stream().allMatch(device -> device.online()
-                    && device.channels().stream().allMatch(channel -> channel.online()
-)));
-            assertTrue(poweredOn.collectors().stream().allMatch(collector -> collector.online()));
-        }
-    }
-
-    @Test
-    void powerChangeImmediatelyReconcilesCollectorStatus() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            DevelopmentCollectorStatusSynchronizer synchronizer = mock(DevelopmentCollectorStatusSynchronizer.class);
-            BtsDevelopmentSimulatorController controller = new BtsDevelopmentSimulatorController(simulator,
-                    new InMemoryDeviceDiscoveryCandidateStore(), synchronizer);
-
-            controller.setPower(new BtsDevelopmentSimulatorController.SimulatorPowerCommand(true));
-
-            verify(synchronizer).synchronizeOnlineStatus();
-        }
-    }
-
-    @Test
-    void simulatorPowerCommandRequiresExplicitState() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-
-            ResponseStatusException exception = assertThrows(ResponseStatusException.class,
-                    () -> controller.setPower(new BtsDevelopmentSimulatorController.SimulatorPowerCommand(null)));
-
-            assertEquals(400, exception.getStatusCode().value());
-            assertTrue(controller.state().collectors().get(0).online());
-        }
-    }
-
-    @Test
-    void virtualIpMustBeUniqueAcrossCabinetsAndCollectors() throws Exception
-    {
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller = controller(simulator);
-
-            assertThrows(org.springframework.web.server.ResponseStatusException.class,
-                    () -> controller.createCollector(
-                            new BtsDevelopmentSimulatorController.CreateCollectorCommand("127.0.0.2", 4)));
-            assertThrows(org.springframework.web.server.ResponseStatusException.class,
-                    () -> controller.createDevice(
-                            new BtsDevelopmentSimulatorController.CreateDeviceCommand("127.0.1.2", 4)));
-        }
-    }
-
-    @Test
-    void developmentControlBoundaryOnlyAllowsLoopbackClients() throws Exception
-    {
-        var interceptor = new BtsDevelopmentSimulatorWebConfig.LocalOnlyInterceptor();
-        MockHttpServletResponse localResponse = new MockHttpServletResponse();
-        MockHttpServletRequest localRequest = requestFrom(InetAddress.getLoopbackAddress().getHostAddress());
-        MockHttpServletResponse remoteResponse = new MockHttpServletResponse();
-        MockHttpServletRequest remoteRequest = requestFrom("192.168.1.20");
-
-        assertTrue(interceptor.preHandle(localRequest, localResponse, new Object()));
-        assertFalse(interceptor.preHandle(remoteRequest, remoteResponse, new Object()));
-        assertEquals(403, remoteResponse.getStatus());
-        assertTrue(BtsDevelopmentSimulatorController.class.isAnnotationPresent(Anonymous.class));
-    }
-
-    @Test
-    void exposesStaticControlPageThroughAnonymousHandler() throws Exception
-    {
-        Method page = BtsDevelopmentSimulatorController.class.getMethod("page");
-
-        assertArrayEquals(new String[] { "/dev/bts-simulator.html" },
-                page.getAnnotation(GetMapping.class).value());
-        assertTrue(BtsDevelopmentSimulatorController.class.isAnnotationPresent(Anonymous.class));
-    }
-
-    @Test
-    void resettingSimulatorRemovesExistingDevelopmentCandidates() throws Exception
-    {
-        Instant now = Instant.parse("2026-08-27T00:00:00Z");
-        InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
-        candidateStore.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-8", 22, 8,
-                "127.0.0.8", null, "旧模拟柜", null, null,
-                List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 8, 1, 1), true,
-                        DeviceWorkStatus.STOPPED, now)), now, now.plusSeconds(30))));
-        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
-        {
-            BtsDevelopmentSimulatorController controller =
-                    new BtsDevelopmentSimulatorController(simulator, candidateStore);
-
-            controller.reset();
-
-            assertEquals(List.of(), candidateStore.snapshot(now).cabinets());
-        }
-    }
-
-    private BtsDevelopmentSimulatorController controller(BtsDevelopmentSimulator simulator)
-    {
-        return new BtsDevelopmentSimulatorController(simulator, new InMemoryDeviceDiscoveryCandidateStore());
-    }
-
-    private MockHttpServletRequest requestFrom(String remoteAddress)
-    {
-        MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dev/bts-simulator/state");
-        request.setRemoteAddr(remoteAddress);
-        return request;
-    }
-}
diff --git a/scripts/clear-simulator-development-data.sql b/scripts/clear-simulator-development-data.sql
new file mode 100644
index 0000000..f7a8e6c
--- /dev/null
+++ b/scripts/clear-simulator-development-data.sql
@@ -0,0 +1,114 @@
+-- 开发模拟器产生的开发设备与开发数据 · 定点清理
+-- ==========================================================================
+-- 背景:开发模拟器(dev-sim)已整体拆除,不再产生任何新数据;本脚本清理它此前产生的开发设备。
+--
+-- ⚠️ 脚本故意不给一条「整段执行」的路径。**先跑「一、先查」,按结果决定后面能做什么。**
+--
+-- 关键事实:当前业务基线(`db/migration-final/V2__final_business_baseline.sql`)**没有外键约束**
+-- (全表 0 个 FOREIGN KEY,旧的 db/migration 已不作为 Flyway 位置)。所以删除设备行**不会被数据库拦下**,
+-- 悬空引用(例如实时记录还指着已删掉的通道)也不会报错——先查段是唯一防线。
+--
+-- 纪律(与 Ticket04「不影响已登记的正式设备、业务事实、审计记录与历史数据」一致):
+--   1. **不整表 TRUNCATE、不按时间范围删除、不删审计/修订记录**,只按模拟器的固定身份处理:
+--      适配器编码 `dev-sim`、采集器编码 `dev-sim-collector-N`。
+--   2. **能在产品页面上做的删除,先在页面上做**:检测柜与采集器的删除动作会走产品的引用校验与逻辑删除口径
+--      (`del_flag`),本脚本不另造一条删除路径。本脚本的 SQL 删除只用于界面上已经没有入口的行
+--      (例如代码装配时代留下的模拟器适配器行)。
+--   3. **绑定了模拟器通道的测试用「测试整体删除」清理**:ADR-0047 的明确无痕例外(一个事务清掉测试全部子数据、
+--      绑定、阈值与数据修订,不写日志与修订)。不要在这里直接 DELETE 测试行,那会留下指向测试的孤儿数据。
+--   4. 删除段只在**先查段的引用计数全为 0** 时执行,分步确认影响行数。有引用却先删了设备行,
+--      那些引用就会变成悬空数据,而且没有任何报错提醒你。
+--   5. 执行前先停止 Spring Boot 后端(避免运行期把它重新写回)。
+--
+-- 交付说明:这些行是否存在取决于此前是否用模拟器登记过设备。先查段计数全为 0 就说明没有模拟器开发设备,
+--          删除段即为空操作。
+-- ==========================================================================
+
+-- 一、先查:确认要删什么、以及必须先经产品路径清掉的关联测试
+-- --------------------------------------------------------------------------
+
+-- 1.1 模拟器适配器行(ADR-0018:dev-sim 是代码装配、不写 t_device_adapter,所以这里通常为 0)
+SELECT COUNT(*) AS dev_sim_adapter_rows
+FROM t_device_adapter
+WHERE adapter_code = 'dev-sim';
+
+-- 1.2 模拟器登记过的检测柜与通道
+SELECT c.cabinet_id, c.cabinet_code, c.cabinet_name, c.adapter_id, c.del_flag
+FROM t_detection_cabinet c
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+SELECT ch.channel_id, ch.cabinet_id, ch.devtype, ch.devid, ch.subdevid, ch.chlid, ch.del_flag
+FROM t_detection_channel ch
+JOIN t_detection_cabinet c ON c.cabinet_id = ch.cabinet_id
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+-- 1.3 模拟器采集器
+SELECT collector_id, collector_code, collector_name, adapter_id, del_flag
+FROM t_collector
+WHERE collector_code LIKE 'dev-sim-collector-%';
+
+-- 1.4 **必须先清掉的关联测试**:下面四个计数都必须为 0,否则先按纪律第 2、3 条在界面上删除这些测试
+--     (删除测试会一并清掉它的绑定、实时/工步/循环事实与采集器映射,也就解开了对设备行的引用)
+
+--     1.4.1 绑定了模拟器通道的测试
+SELECT DISTINCT t.test_id, t.test_name, t.test_status, t.reception_status
+FROM t_test t
+JOIN t_test_channel_binding b ON b.test_id = t.test_id
+JOIN t_detection_channel ch ON ch.channel_id = b.channel_id
+JOIN t_detection_cabinet c ON c.cabinet_id = ch.cabinet_id
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+--     1.4.2 仍指向模拟器通道的测试通道绑定
+SELECT COUNT(*) AS binding_rows
+FROM t_test_channel_binding b
+JOIN t_detection_channel ch ON ch.channel_id = b.channel_id
+JOIN t_detection_cabinet c ON c.cabinet_id = ch.cabinet_id
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+--     1.4.3 仍指向模拟器设备或采集器的实时记录
+SELECT COUNT(*) AS realtime_snapshot_rows
+FROM t_test_realtime_record r
+WHERE r.collector_id IN (SELECT collector_id FROM t_collector WHERE collector_code LIKE 'dev-sim-collector-%')
+   OR (r.adapter_code = 'dev-sim');
+
+--     1.4.4 仍指向模拟器采集器的测试-采集器映射
+SELECT COUNT(*) AS collector_mapping_rows
+FROM t_test_collector_channel m
+JOIN t_collector col ON col.collector_id = m.collector_id
+WHERE col.collector_code LIKE 'dev-sim-collector-%';
+
+-- 二、删除:**仅当 1.4 的引用计数全为 0** 才执行;分步执行并确认影响行数
+-- --------------------------------------------------------------------------
+
+-- 2.1 模拟器通道(先删子表)
+DELETE ch FROM t_detection_channel ch
+JOIN t_detection_cabinet c ON c.cabinet_id = ch.cabinet_id
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+-- 2.2 模拟器检测柜
+DELETE c FROM t_detection_cabinet c
+JOIN t_device_adapter a ON a.adapter_id = c.adapter_id
+WHERE a.adapter_code = 'dev-sim';
+
+-- 2.3 模拟器采集器(含已被页面逻辑删除的行)
+DELETE FROM t_collector WHERE collector_code LIKE 'dev-sim-collector-%';
+
+-- 2.4 模拟器适配器行(若 1.1 计为 0,这一句是空操作)
+DELETE FROM t_device_adapter WHERE adapter_code = 'dev-sim';
+
+-- ==========================================================================
+-- 三、Redis 里的模拟器状态(在 Redis 上执行,不在业务库里)
+-- --------------------------------------------------------------------------
+-- 这些键是模拟器的运行状态,拆除后不再有人写它们:
+--   redis-cli DEL dev:bts-simulator:state:v1
+--   redis-cli DEL dev:bts-simulator:recent-requests:v1
+--   redis-cli DEL dev:bts-simulator:collector-history:v1
+-- **不要删** dev:device-discovery:candidates:v1 —— 它是正式发现候选(RedisDeviceDiscoveryCandidateStore)
+-- 用的同一个键;模拟器时代写进去的采集器候选已随 collectors 字段停止恢复(代码读到时只忽略),
+-- 正式检测柜候选必须保留。
+-- ==========================================================================

--
Gitblit v1.7.1