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/PushIngestReceptionContractTest.java |   73 +++++++++++++++++++++++++++++++++++-
 1 files changed, 71 insertions(+), 2 deletions(-)

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)

--
Gitblit v1.7.1