| | |
| | | {"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); |
| | |
| | | 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) |