yeg
5 天以前 99935242ecccd94e76978e2b71cbc46b87d31dc6
battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
@@ -16,7 +16,9 @@
import org.junit.jupiter.api.Test;
import com.battery.device.api.DeviceAdapterException;
import com.battery.device.api.TestDataObservationAdapter.CabinetRealtime;
import com.battery.device.api.TestDataObservationAdapter.CycleRecord;
import com.battery.device.api.TestDataObservationAdapter.SampleWatermark;
import com.battery.device.api.TestDataObservationAdapter.StepRecord;
import com.battery.device.api.model.AdapterCapabilities;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DetectedChannel;
@@ -45,6 +47,37 @@
               "Cycle_Index":1,"Voltage":1.2701,"eCurrent":0.5,"ChargeCapacity":0.121,"ChargeEnergy":0.31},
              {"record_time":"134349698000000000","Data_Point":422,"Step_Time":600.0,"Step_Index":5,
               "Cycle_Index":1,"Voltage":1.2702,"eCurrent":-0.4,"DischargeCapacity":0.05,"DischargeEnergy":0.1}]}
            """;
    /**
     * 两个循环的采样行:循环 1 是充电、搁置、放电三段(都结束了),循环 2 的第一段还在进行。
     * 容量列是工步累计:充电段从 0.10 涨到 0.12,放电段从 0.05 涨到 0.09。
     */
    private static final String TWO_CYCLES = """
            {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
              {"record_time":"134349692000000000","Data_Point":420,"Step_Time":540.0,"Step_Index":1,"Cycle_Index":1,
               "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10,"ChargeEnergy":0.30},
              {"record_time":"134349698000000000","Data_Point":421,"Step_Time":600.0,"Step_Index":1,"Cycle_Index":1,
               "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36},
              {"record_time":"134349704000000000","Data_Point":422,"Step_Time":1200.0,"Step_Index":2,"Cycle_Index":1,
               "Voltage":1.25,"eCurrent":0.0,"ChargeCapacity":0.12,"DischargeCapacity":0.0},
              {"record_time":"134349764000000000","Data_Point":423,"Step_Time":1800.0,"Step_Index":3,"Cycle_Index":1,
               "Voltage":1.24,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.05,"DischargeEnergy":0.15},
              {"record_time":"134349824000000000","Data_Point":424,"Step_Time":3600.0,"Step_Index":3,"Cycle_Index":1,
               "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.09,"DischargeEnergy":0.27},
              {"record_time":"134349830000000000","Data_Point":425,"Step_Time":60.0,"Step_Index":1,"Cycle_Index":2,
               "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01,"ChargeEnergy":0.02}]}
            """;
    /** 第一段的电流正负混用:方向判不出来,明细照留但不进循环的任何一侧。 */
    private static final String MIXED_SIGN = """
            {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
              {"record_time":"134349692000000000","Data_Point":420,"Step_Index":1,"Cycle_Index":1,
               "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10},
              {"record_time":"134349698000000000","Data_Point":421,"Step_Index":1,"Cycle_Index":1,
               "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.10,"DischargeCapacity":0.02},
              {"record_time":"134349704000000000","Data_Point":422,"Step_Index":2,"Cycle_Index":1,
               "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36},
              {"record_time":"134349830000000000","Data_Point":425,"Step_Index":1,"Cycle_Index":2,
               "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01}]}
            """;
    private final FakeIngestLanding landing = new FakeIngestLanding();
@@ -172,21 +205,118 @@
    /** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */
    @Test
    void collectorAndStepCycleObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics()
    void collectorObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics()
    {
        ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
        ingest(BATCH);
        PushIngestAdapter adapter = adapter(NOW);
        assertNull(adapter.latestCollector(ADAPTER, "collector-1"));
        assertTrue(adapter.stepRecords(channel()).isEmpty());
        assertTrue(adapter.cycleRecords(channel()).isEmpty());
        // 落地表在本地,取数不产生厂商负载,所以按展示节奏取数;厂商写库节奏仍是 60 秒。
        assertEquals(5000L, adapter.persistenceIntervalMillis(channel()));
        // 工步与循环的重算跟着来源的写库节奏走,不跟实时行的展示节奏:源头最快 60 秒才产出一行。
        assertEquals(60000L, adapter.aggregateIntervalMillis(channel()));
        AdapterCapabilities capabilities = adapter.capabilities();
        assertTrue(capabilities.discovery() && capabilities.realtimeObservation() && !capabilities.control());
    }
    /**
     * 来源没有工步层,工步与循环由采样行分段推导(ADR-0067):已结束的段各交出一条工步记录,
     * 取值来自该段末行,方向由该段自己的电流符号判定;最新一段仍在进行,不交出去。
     */
    @Test
    void closedSampleSegmentsBecomeStepRecordsWhileTheNewestSegmentStaysUnpublished()
    {
        ingest(BATCH);
        PushIngestAdapter adapter = adapter(NOW);
        List<StepRecord> steps = adapter.stepRecords(channel());
        // 三行落地行分成两段:(循环 1, 工步 4) 已结束,(循环 1, 工步 5) 还在进行。
        assertEquals(1, steps.size());
        StepRecord step = steps.get(0);
        assertEquals(1, step.cycleNo());
        assertEquals(4, step.stepNo());
        // 方向取自该段电流符号:段内两行恒正即充电。
        assertEquals("charge", step.stepType());
        // 容量与能量是工步累计,取该段末行的值;工步时间是工步内相对时间(秒)。
        assertEquals(0.121, step.capacityAh(), 1e-9);
        assertEquals(0.31, step.energyWh(), 1e-9);
        assertEquals(600.0, step.measurements().get("stepDuration").doubleValue(), 1e-9);
        assertEquals(1.26778, step.measurements().get("startVoltage").doubleValue(), 1e-9);
        // 设备没给的列不写零占位:该段只有充电侧。
        assertFalse(step.measurements().containsKey("dischargeCapacity"));
        assertEquals(1.2701, step.endVoltageV(), 1e-9);
        assertEquals(0.5, step.endCurrentA(), 1e-9);
        assertEquals(Instant.parse("2026-09-27T07:53:20Z"), step.startedAt());
        assertEquals(Instant.parse("2026-09-27T08:03:20Z"), step.endedAt());
        // 接收时间取我们收到该段末行的时刻(与推式实时路径同一口径),设备原始时间照留。
        assertEquals(NOW, step.serverReceivedAt());
        assertEquals("134349698000000000", step.deviceTimeRaw());
        // 来源行身份含来源库与段标识:同一次测试重算多少次都得到同一个身份。
        assertEquals("push:DB_20260927_155209/Ws26_IV_0:step:1:4", step.sourceRecordId());
        // 进行中的那一段所属的循环整体不交:半个循环不是事实。
        assertTrue(adapter.cycleRecords(channel()).isEmpty());
    }
    /**
     * 循环由已结束的工步按方向求和(搁置段两向都不增长,贡献 0),效率是放电容量与充电容量之比;
     * 推导值带来源类别,界面与导出据此标注它是聚合出来的。
     */
    @Test
    void cyclesAggregateClosedStepsByDirectionAndSkipTheRunningCycle()
    {
        ingest(TWO_CYCLES);
        PushIngestAdapter adapter = adapter(NOW);
        List<StepRecord> steps = adapter.stepRecords(channel());
        List<CycleRecord> cycles = adapter.cycleRecords(channel());
        // 循环 1 的三段都已结束(后面已经出现循环 2 的第一段);循环 2 的第一段仍在进行。
        assertEquals(List.of("1:1:charge", "1:2:rest", "1:3:discharge"), steps.stream()
                .map(step -> step.cycleNo() + ":" + step.stepNo() + ":" + step.stepType()).toList());
        // 搁置段自身的累计是 0(该段什么都没充放),不是缺失。
        assertEquals(0.0, steps.get(1).capacityAh(), 1e-9);
        assertEquals(0.0, steps.get(1).energyWh(), 1e-9);
        assertEquals(List.of(1), cycles.stream().map(CycleRecord::cycleNo).toList());
        CycleRecord cycle = cycles.get(0);
        assertEquals(0.12, cycle.chargeCapacityAh(), 1e-9);
        assertEquals(0.09, cycle.dischargeCapacityAh(), 1e-9);
        assertEquals(0.36, cycle.chargeEnergyWh(), 1e-9);
        assertEquals(0.27, cycle.dischargeEnergyWh(), 1e-9);
        // 效率是百分数:0.09 / 0.12。
        assertEquals(75.0, cycle.efficiency(), 1e-9);
        assertEquals(CycleRecord.SOURCE_AGGREGATED, cycle.sourceKind());
        assertEquals("push:DB_20260927_155209/Ws26_IV_0:cycle:1", cycle.sourceRecordId());
    }
    /** 方向判不出来时(段内电流正负混用)明细照留、单项容量留空,也不进循环的任何一侧。 */
    @Test
    void segmentsWithMixedCurrentSignsStayOutOfTheCycleAggregate()
    {
        ingest(MIXED_SIGN);
        PushIngestAdapter adapter = adapter(NOW);
        List<StepRecord> steps = adapter.stepRecords(channel());
        List<CycleRecord> cycles = adapter.cycleRecords(channel());
        StepRecord undecided = steps.get(0);
        assertEquals(1, undecided.cycleNo());
        assertEquals(1, undecided.stepNo());
        // 方向不唯一时不猜:类型留空、单项容量与能量留空。
        assertNull(undecided.stepType());
        assertNull(undecided.capacityAh());
        assertNull(undecided.energyWh());
        // 四组容量能量作为设备事实照原样保留,供核对。
        assertEquals(0.02, undecided.measurements().get("dischargeCapacity").doubleValue(), 1e-9);
        // 该段不进任何一侧:循环 1 的放电侧算出来是 0,而不是把这一段里的 0.02 算进去。
        assertEquals(1, cycles.size());
        assertEquals(0.12, cycles.get(0).chargeCapacityAh(), 1e-9);
        assertEquals(0.0, cycles.get(0).dischargeCapacityAh(), 1e-9);
    }
    /** 环境量与量纲确认后落到配置:列名、指标编码、单位、采集器通道都由声明给出,代码里没有厂商列名。 */
    @Test
    void declaredAuxiliaryMetricIsProjectedWithItsOwnSourceIdentityValueUnitAndDeviceTime()