yeg
6 天以前 e38aca8134570513435c003c25dc05f1ae51c9c9
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)