From 691f882740b46c5354fb33404fef6efb370e05dc Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 09 十月 2026 15:52:05 +0800
Subject: [PATCH] 实时行也带上判定出的方向,实时页的工步名称才有值

---
 battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java |  147 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 144 insertions(+), 3 deletions(-)

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 ec6c1be..3dd5d21 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
@@ -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();
@@ -124,6 +157,10 @@
         assertEquals(5, latest.stepNo());
         // 放电容量的取法按电流方向,与直连来源同一口径;来源行身份供幂等与冲突判定。
         assertEquals(0.05, latest.capacityAh(), 1e-9);
+        // 实时行也带方向:类型与名称都按电流符号判定(该行电流为负 → 放电),
+        // 实时页的"工步名称"列用的就是它(用户对照甲方那一列「状态」)。
+        assertEquals("discharge", latest.stepType());
+        assertEquals("放电", latest.stepName());
         assertEquals("push:DB_20260927_155209/Ws26_IV_0:422", latest.sourceRecordId());
 
         // 实时快照:通道工作状态在读落地表路线下不可观测(ADR-0063)。
@@ -172,21 +209,125 @@
 
     /** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */
     @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());
+        // 工步名称用同一套词(甲方软件那一列「状态」显示的就是充电/放电/搁置):该来源的方案里
+        // 只有「步_N」标号,这个名称是判定出的方向,不是设备给的名称。
+        assertEquals("充电", step.stepName());
+        // 容量与能量是工步累计,取该段末行的值;工步时间是工步内相对时间(秒)。
+        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("搁置", steps.get(1).stepName());
+        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.stepName());
+        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()

--
Gitblit v1.7.1