yeg
2026-09-09 a011865a0989b51d5818f2791d04790b6408acd1
按甲方字段补齐实时相对时长持久化

在导入、设备观测、持久化及查询中独立传递相对时长,保留历史空值和原有统计来源。补充真实表格、重复冲突及设备字段映射回归。
8个文件已修改
62 ■■■■■ 已修改文件
battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TestDataQueryServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-device-api/src/main/java/com/battery/device/api/TestDataObservationAdapter.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-device-simulator/src/test/java/com/battery/device/contract/SimulatorPublicContractTest.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/TestDataReceptionServiceImplTest.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java
@@ -310,6 +310,12 @@
        }
        if ("REALTIME".equals(type))
        {
            // 相对时长是来源事实,独立于绝对设备时间;旧记录缺失不补算。
            BigDecimal relativeTime = importMetric(row, "relativeTimeSeconds");
            if (relativeTime != null && relativeTime.signum() < 0) throw new ServiceException("列「记录时间(h:min:s.ms)」:不能为负数");
            Map<String, Object> extension = new LinkedHashMap<>();
            extension.put("relativeTimeSeconds", relativeTime);
            values.put("extension_measurements_json", JSON.toJSONString(extension));
            values.put("cabinet_sequence_id", importInteger(row, "sequenceId", "记录序号", 9007199254740991L));
            for (String field : List.of("devtype", "devid", "subdevid", "chlid")) values.put("cabinet_" + field, binding.get(field));
            // 导入不是轮询采样;同秒可有不同记录,设备时间不能为槽位唯一性而偏移。
@@ -360,6 +366,7 @@
    private String metricLabel(String field)
    {
        return switch (field) {
            case "relativeTimeSeconds" -> "记录时间(h:min:s.ms)";
            case "voltage" -> "电压(V)"; case "current" -> "电流(A)"; case "capacity" -> "容量(Ah)"; case "energy" -> "能量(Wh)";
            case "temperature" -> "温度(℃)"; case "positivePotential" -> "正电位(V)"; case "negativePotential" -> "负电位(V)";
            case "endVoltage" -> "终止电压(V)"; case "startVoltage" -> "起始电压(V)"; case "stepDuration" -> "工步时间";
battery-business/src/main/java/com/battery/business/service/impl/TestDataQueryServiceImpl.java
@@ -25,7 +25,14 @@
    @Override public List<Map<String, Object>> records(Long testId)
    {
        tests.selectTestById(testId);
        return normalizeApiTimes(mapper.selectPersistedFinalRealtimeRecords(testId));
        List<Map<String, Object>> rows = normalizeApiTimes(mapper.selectPersistedFinalRealtimeRecords(testId));
        for (Map<String, Object> row : rows)
        {
            Object extension = row.get("extensionMeasurementsJson");
            if (extension != null) row.put("relativeTimeSeconds",
                    com.alibaba.fastjson2.JSON.parseObject(extension.toString()).get("relativeTimeSeconds"));
        }
        return rows;
    }
    /** 查询尚存工步记录,包含设备统计与授权历史导入。 */
battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java
@@ -130,6 +130,8 @@
        if (cabinet != null) { row.setCycleNo(cabinet.cycleNo()); row.setStepNo(cabinet.stepNo()); row.setStepType(cabinet.stepType()); row.setStepName(cabinet.stepName()); row.setVoltageV(BigDecimal.valueOf(cabinet.voltageV())); row.setCurrentA(BigDecimal.valueOf(cabinet.currentA())); row.setCapacityAh(BigDecimal.valueOf(cabinet.capacityAh())); row.setEnergyWh(BigDecimal.valueOf(cabinet.energyWh())); }
        row.setTemperature(metricValue(mapped, "TEMPERATURE")); row.setPositivePotential(metricValue(mapped, "POSITIVE_POTENTIAL")); row.setNegativePotential(metricValue(mapped, "NEGATIVE_POTENTIAL")); row.setCollectorChannelCode(mapped.values().stream().map(CollectorMeasurement::channelCode).findFirst().orElse(null));
        Map<String, Object> extension = new LinkedHashMap<>();
        // 工步内相对时长由成功柜端观测提供,不以轮询槽位或服务器时间推算。
        if (cabinet != null && cabinet.relativeTimeSeconds() != null) extension.put("relativeTimeSeconds", cabinet.relativeTimeSeconds());
        for (Map.Entry<String, CollectorMeasurement> e : mapped.entrySet()) if (!isFixedMetric(e.getKey()))
        {
            Map<String, Object> value = new LinkedHashMap<>(); value.put("value", e.getValue().value()); value.put("unit", e.getValue().unit()); value.put("channelCode", e.getValue().channelCode()); extension.put(e.getKey(), value);
battery-device-api/src/main/java/com/battery/device/api/TestDataObservationAdapter.java
@@ -31,9 +31,10 @@
    record PlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds,
            Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
    /** 柜端成功观测;相对时长使用设备提供的秒数,缺失不按采样时间推算。 */
    record CabinetRealtime(long sequenceId, Instant deviceTime, double currentA, double voltageV,
            double capacityAh, double energyWh, int cycleNo, int stepNo, String stepName, String stepType,
            Instant serverReceivedAt, String deviceTimeRaw, String timezoneId) { }
            Instant serverReceivedAt, String deviceTimeRaw, String timezoneId, Long relativeTimeSeconds) { }
    record CollectorRealtime(String collectorCode, boolean online, int sampleIntervalMs,
            List<CollectorMeasurement> measurements, String timezoneId)
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
@@ -362,7 +362,7 @@
        CabinetRealtime result = new CabinetRealtime(sequence, now, configuration.currentA().doubleValue(),
                configuration.voltageV().doubleValue(), configuration.capacityAh().doubleValue(), configuration.energyWh().doubleValue(),
                configuration.cycleNo(), configuration.stepNo(), step.name(), step.stepType(),
                now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId());
                now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId(), configuration.relativeTimeSeconds());
        return result;
    }
battery-device-simulator/src/test/java/com/battery/device/contract/SimulatorPublicContractTest.java
@@ -197,6 +197,7 @@
            assertEquals("dc", observation.stepType());
            assertEquals(120L, observation.totalTimeSeconds());
            assertEquals(30L, observation.relativeTimeSeconds());
            assertEquals(30L, simulator.latestCabinet(channel).relativeTimeSeconds());
        }
    }
ruoyi-admin/src/test/java/com/battery/business/service/impl/TestDataReceptionServiceImplTest.java
@@ -145,6 +145,7 @@
        assertEquals("dev-sim:22:1:1:1", row.getSourceIdentity());
        assertEquals("cabinet:7:collector:10", row.getSourceRecordId());
        assertEquals(Date.from(Instant.parse("2026-09-02T04:00:30Z")), row.getCabinetDeviceTimeUtc());
        assertEquals(3752L, com.alibaba.fastjson2.JSON.parseObject(row.getExtensionMeasurementsJson()).getLong("relativeTimeSeconds"));
        assertEquals(Date.from(Instant.parse("2026-09-02T04:00:40Z")), row.getCollectorDeviceTimeUtc());
        assertEquals(new BigDecimal("25.100"), row.getTemperature());
        assertEquals(new BigDecimal("1.620"), row.getPositivePotential());
@@ -167,6 +168,7 @@
        ArgumentCaptor<TTestRealtimeRecord> saved = ArgumentCaptor.forClass(TTestRealtimeRecord.class);
        verify(fixture.mapper).insertRealtimeRecord(saved.capture());
        assertNull(saved.getValue().getCabinetSequenceId());
        assertNull(com.alibaba.fastjson2.JSON.parseObject(saved.getValue().getExtensionMeasurementsJson()).get("relativeTimeSeconds"));
        assertNull(saved.getValue().getCabinetDevtype());
        assertNull(saved.getValue().getCabinetAdapterTimezoneId());
        assertNull(saved.getValue().getVoltageV());
@@ -183,7 +185,9 @@
        Instant started = Instant.parse("2026-09-02T04:00:10Z");
        Instant ended = Instant.parse("2026-09-02T04:00:50Z");
        when(fixture.simulator.stepRecords(any(ChannelIdentity.class))).thenReturn(List.of(new StepRecord(
                "STEP-1", 1, 2, "恒流充电", "cc", started, ended, 3.4, 2.5, 8.4, ended.toString(), "UTC", ended, java.util.Map.of())));
                "STEP-1", 1, 2, "恒流充电", "cc", started, ended, 3.4, 2.5, 8.4, ended.toString(), "UTC", ended,
                java.util.Map.of("stepDuration", new BigDecimal("40.125"), "startVoltage", new BigDecimal("3.1"),
                        "chargeMedianVoltage", new BigDecimal("3.25"), "dischargeMedianVoltage", BigDecimal.ZERO))));
        when(fixture.simulator.cycleRecords(any(ChannelIdentity.class))).thenReturn(List.of(new CycleRecord(
                "CYCLE-1-1-1", 1, started, ended, 2.5, 2.4, 8.4, 8.0, 96, ended.toString(), "UTC", ended)));
        when(fixture.mapper.insertStepRecord(any())).thenReturn(1);
@@ -196,6 +200,11 @@
        verify(fixture.mapper).insertStepRecord(step.capture());
        verify(fixture.mapper).insertCycleRecord(cycle.capture());
        assertEquals("record:STEP-1", step.getValue().getDedupeKey());
        var metrics = com.alibaba.fastjson2.JSON.parseObject(step.getValue().getExtensionMeasurementsJson());
        assertEquals(new BigDecimal("40.125"), metrics.getBigDecimal("stepDuration"));
        assertEquals(new BigDecimal("3.1"), metrics.getBigDecimal("startVoltage"));
        assertEquals(new BigDecimal("3.25"), metrics.getBigDecimal("chargeMedianVoltage"));
        assertEquals(BigDecimal.ZERO, metrics.getBigDecimal("dischargeMedianVoltage"));
        assertEquals("CYCLE-1-1-1", cycle.getValue().getSourceRecordId());
        assertEquals("record:CYCLE-1-1-1", cycle.getValue().getDedupeKey());
        assertEquals(Date.from(ended), step.getValue().getServerReceivedAtUtc());
@@ -324,7 +333,7 @@
    {
        Instant deviceTime = Instant.parse("2026-09-02T04:00:30Z");
        return new CabinetRealtime(7L, deviceTime, 1, 3.2, 2.5, 8.4,
                1, 2, "恒流充电", "cc", Instant.parse("2026-09-02T04:00:35Z"), "2026-09-02 12:00:30.000", "Asia/Hong_Kong");
                1, 2, "恒流充电", "cc", Instant.parse("2026-09-02T04:00:35Z"), "2026-09-02 12:00:30.000", "Asia/Hong_Kong", 3752L);
    }
    private static CollectorMeasurement collector(String channel, long sequenceId, String metric,
ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java
@@ -348,14 +348,14 @@
            assertEquals((long) count, service.selectActionById(actions.get(0).getActionId()).getAffectedRecordCount());
            String label = switch (type) { case "REALTIME" -> "实时"; case "STEP" -> "工步"; default -> "循环"; };
            assertEquals("导入" + count + "条" + label + "数据", actions.get(0).getScopeSummary());
            if ("STEP".equals(type))
            if ("STEP".equals(type) || "REALTIME".equals(type))
            {
                var persisted = mapper.selectImportDataRecords(1L, type).stream().collect(java.util.stream.Collectors.toMap(
                        record -> record.get("source_record_id").toString(), record -> record));
                for (var input : rows)
                {
                    var stored = JSON.parseObject(persisted.get(input.get("recordNo").toString()).get("extension_measurements_json").toString());
                    for (String field : List.of("chargeMedianVoltage", "dischargeMedianVoltage"))
                    for (String field : "STEP".equals(type) ? List.of("stepDuration", "chargeMedianVoltage", "dischargeMedianVoltage") : List.of("relativeTimeSeconds"))
                        if (input.get(field) == null) assertNull(stored.get(field));
                        else assertEquals(0, new java.math.BigDecimal(input.get(field).toString()).compareTo(stored.getBigDecimal(field)), field);
                }
@@ -364,6 +364,27 @@
        assertEquals(3, service.selectActions(new TTestDataRevisionAction()).size());
    }
    @Test void realtimeRelativeTimeSurvivesImportQueryAndDuplicateValidation()
    {
        var input = row(); input.put("relativeTimeSeconds", 3752.125);
        submit("REALTIME", List.of(input));
        var stored = mapper.selectImportDataRecords(1L, "REALTIME").get(0);
        assertNotNull(stored.get("extension_measurements_json"));
        var extension = JSON.parseObject(stored.get("extension_measurements_json").toString());
        assertEquals(0, new java.math.BigDecimal("3752.125").compareTo(extension.getBigDecimal("relativeTimeSeconds")));
        var queryMapper = org.mockito.Mockito.mock(TTestMapper.class);
        var query = new com.battery.business.service.impl.TestDataQueryServiceImpl(queryMapper,
                org.mockito.Mockito.mock(com.battery.business.service.ITTestService.class));
        var current = new LinkedHashMap<String, Object>(); current.put("extensionMeasurementsJson", extension.toJSONString());
        var legacy = new LinkedHashMap<String, Object>();
        org.mockito.Mockito.when(queryMapper.selectPersistedFinalRealtimeRecords(1L)).thenReturn(List.of(current, legacy));
        assertEquals(new java.math.BigDecimal("3752.125"), query.records(1L).get(0).get("relativeTimeSeconds"));
        assertNull(query.records(1L).get(1).get("relativeTimeSeconds"));
        assertEquals(1, submit("REALTIME", List.of(input)).get("skippedCount"));
        input.put("relativeTimeSeconds", 3752.126);
        assertThrows(RuntimeException.class, () -> submit("REALTIME", List.of(input)));
    }
    private Map<String, Object> row()
    {
        var row = new LinkedHashMap<String, Object>(Map.of("recordNo", "17", "batteryNo", "BAT-1", "recordedAt", 1683517331000L,