yeg
2026-09-05 07d3e4aa6478fe61edb271d80cbc61d037e6f409
对齐控制台时间与扩展测量响应

测试详情、修订与实时流统一输出毫秒时间,补充采集器设备时间和扩展测量字段,并验证业务序列化边界。
1个文件已添加
7个文件已修改
89 ■■■■■ 已修改文件
ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java
New file
@@ -0,0 +1,51 @@
package com.ruoyi.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestDataRevisionAction;
import com.ruoyi.system.service.ITestLiveService.LiveState;
/** 控制台 REST/SSE 的业务时间统一为毫秒,平台响应保持原有格式。 */
class ConsoleTimeContractTest
{
    private final JsonMapper json = JsonMapper.builder().findAndAddModules().build();
    private final long time = 1788588000123L;
    @Test
    void testDetailAndRevisionExposeEpochMillis()
    {
        TTest test = new TTest();
        test.setCreateTime(new Date(time));
        test.setUpdateTime(new Date(time));
        test.setStartedAtUtc(new Date(time));
        test.setEndedAtUtc(new Date(time));
        var detail = json.readTree(json.writeValueAsString(AjaxResult.success(test))).get("data");
        for (String field : List.of("createTime", "updateTime", "startedAtUtc", "endedAtUtc"))
        {
            assertTrue(detail.get(field).isIntegralNumber(), field);
            assertEquals(time, detail.get(field).longValue());
        }
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setCreatedAtUtc(new Date(time));
        assertEquals(time, json.readTree(json.writeValueAsString(action)).get("createdAtUtc").longValue());
        BaseEntity platform = new BaseEntity();
        platform.setCreateTime(new Date(time));
        assertTrue(json.readTree(json.writeValueAsString(platform)).get("createTime").isString());
    }
    @Test
    void snapshotAndHeartbeatExposeTheSameMillis()
    {
        LiveState state = new LiveState(42L, "RUNNING", "RECEIVING", time, List.of(), List.of(), null);
        assertEquals(time, json.readTree(json.writeValueAsString(state)).get("updatedAt").longValue());
        assertEquals(time, json.readTree(json.writeValueAsString(Map.of("at", state.updatedAt()))).get("at").longValue());
    }
}
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java
@@ -61,6 +61,7 @@
        assertEquals(new BigDecimal("3.700"), snapshot.get("voltageV"));
        assertEquals(new BigDecimal("1.200"), snapshot.get("currentA"));
        assertEquals(new BigDecimal("2.500"), snapshot.get("capacityAh"));
        assertEquals(now.toEpochMilli(), snapshot.get("observedAt"));
    }
    @Test
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java
@@ -36,9 +36,12 @@
    private String testStatus;
    private String receptionStatus;
    private Integer monitorTimeoutMinutes;
    @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    private Date startedAtUtc;
    /** 最近一次开始或继续接收动作建立的服务器时间边界。 */
    @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    private Date receptionBoundaryAtUtc;
    @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    private Date endedAtUtc;
    /** 是否从已运行设备的后续观测边界接入。 */
    private Boolean runtimeAttach;
@@ -67,6 +70,12 @@
    private String consoleDetailJson;
    private TTestConsoleDetail consoleDetail;
    /** 测试业务时间输出毫秒,平台实体仍保留原有时间格式。 */
    @Override @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    public Date getCreateTime() { return super.getCreateTime(); }
    @Override @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    public Date getUpdateTime() { return super.getUpdateTime(); }
    public Long getTestId() { return testId; }
    public void setTestId(Long value) { testId = value; }
    public String getTestName() { return testName; }
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
@@ -20,7 +20,7 @@
    private String reason;
    private Long operatorId;
    private String operatorName;
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
    @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    private Date createdAtUtc;
    /** 查询展示字段:来源测试名称。 */
    private String testName;
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java
@@ -13,12 +13,12 @@
    /** 打开实时 SSE;连接初次发送当前事实,后续只推送最新快照。 */
    SseEmitter stream(Long testId);
    record LiveState(Long testId, String testStatus, String receptionStatus, Instant updatedAt,
    record LiveState(Long testId, String testStatus, String receptionStatus, long updatedAt,
            List<LiveSource> sources, List<Map<String, Object>> exceptions, TestPlanSnapshot testPlan)
    {
        public LiveState(Long testId, String testStatus, String receptionStatus, Instant updatedAt,
                List<LiveSource> sources, List<Map<String, Object>> exceptions)
        { this(testId, testStatus, receptionStatus, updatedAt, sources, exceptions, null); }
        { this(testId, testStatus, receptionStatus, updatedAt.toEpochMilli(), sources, exceptions, null); }
    }
    record TestPlanSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount,
            List<TestPlanRow> rows) { }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
@@ -78,7 +78,7 @@
        }
        String testStatus = test.getTestStatus();
        TestPlanSnapshot testPlan = detectorTestPlan(test);
        return new LiveState(testId, testStatus, test.getReceptionStatus(), Instant.now(), sources, exceptions, testPlan);
        return new LiveState(testId, testStatus, test.getReceptionStatus(), Instant.now().toEpochMilli(), sources, exceptions, testPlan);
    }
    private TestPlanSnapshot detectorTestPlan(TTest test)
@@ -181,7 +181,7 @@
        Map<String, Object> value = new LinkedHashMap<>();
        if (channel.latestSnapshot() == null) return value;
        value.put("source", "检测柜");
        value.put("observedAt", channel.latestSnapshot().receivedAt());
        value.put("observedAt", channel.latestSnapshot().receivedAt().toEpochMilli());
        // BTS 核心采样字段是快照的一等事实,不能只依赖动态 measurements 展示。
        value.put("currentA", channel.latestSnapshot().currentA());
        value.put("voltageV", channel.latestSnapshot().voltageV());
@@ -214,10 +214,19 @@
        {
            Map<String, Object> value = new LinkedHashMap<>();
            value.put("source", "采集器"); value.put("sampleIntervalMs", collector.sampleIntervalMs());
            value.put("measurements", collector.latestMeasurements());
            value.put("measurements", collector.latestMeasurements().stream().map(item -> {
                Map<String, Object> measurement = new LinkedHashMap<>();
                measurement.put("metricCode", item.metricCode());
                measurement.put("channelCode", item.channelCode());
                measurement.put("value", item.value());
                measurement.put("unit", item.unit());
                measurement.put("deviceTime", item.deviceTime().toEpochMilli());
                measurement.put("serverReceivedAt", item.serverReceivedAt().toEpochMilli());
                return measurement;
            }).toList());
            value.put("units", collector.latestMeasurements().stream().map(item -> item.unit()).distinct().toList());
            collector.latestMeasurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo)
                    .ifPresent(observedAt -> value.put("observedAt", observedAt));
                    .ifPresent(observedAt -> value.put("observedAt", observedAt.toEpochMilli()));
            return new LiveSource("COLLECTOR", (long) collector.collectorId(), battery.getBatteryNo(),
                    collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
        }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
@@ -78,7 +78,7 @@
    {
        if (rows == null) return List.of();
        for (Map<String, Object> row : rows)
            for (String key : List.of("deviceTime", "recordedAt", "absoluteTime", "serverReceivedAt"))
            for (String key : List.of("deviceTime", "collectorDeviceTime", "captureSlot", "recordedAt", "absoluteTime", "serverReceivedAt"))
            {
                Object value = row.get(key);
                if (value instanceof Date date) row.put(key, date.getTime());
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -451,10 +451,11 @@
               concat('RT-', r.record_id) recordNo, 'SNAPSHOT' sourceType, b.battery_no batteryNo,
               r.cycle_no cycleNo, r.step_no stepNo, r.step_name stepName, r.step_type stepType,
               r.cabinet_sequence_id sequenceId, r.cabinet_device_time_utc deviceTime,
               r.collector_device_time_utc collectorDeviceTime, r.capture_slot_utc captureSlot,
               r.server_received_at_utc serverReceivedAt, r.voltage_v voltage, r.current_a current,
               r.capacity_ah capacity, r.energy_wh energy, r.temperature temperature,
               r.positive_potential positivePotential, r.negative_potential negativePotential,
               cast(r.extension_measurements_json as char) extensionMeasurements,
               cast(r.extension_measurements_json as char) extensionMeasurementsJson,
               r.adapter_code adapterCode, r.cabinet_devtype devtype, r.cabinet_devid deviceId,
               r.cabinet_subdevid subdevid, r.cabinet_chlid channelId, r.collector_id collectorId,
               r.collector_channel_code channelCode, r.source_identity sourceIdentity, r.source_record_id sourceRecordId