From 07d3e4aa6478fe61edb271d80cbc61d037e6f409 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 05 九月 2026 11:45:36 +0800
Subject: [PATCH] 对齐控制台时间与扩展测量响应

---
 ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java                          |    9 ++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java     |   17 ++++++--
 ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java              |    4 +-
 ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml                          |    3 +
 ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java        |    2 
 ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java  |    1 
 ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java                |   51 +++++++++++++++++++++++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java |    2 
 8 files changed, 80 insertions(+), 9 deletions(-)

diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java
new file mode 100644
index 0000000..1d4e812
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/ConsoleTimeContractTest.java
@@ -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());
+    }
+}
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java
index 01cbf4f..dcc55fe 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java
+++ b/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
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java
index 3704d5c..7ca4504 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java
+++ b/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; }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
index 89497cb..16a4069 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
+++ b/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;
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java
index 26cd372..d39ebc9 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java
+++ b/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) { }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
index 02cd9b9..34cf5e1 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
+++ b/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());
         }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
index 5d28386..7cb8cd8 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
+++ b/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());
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index e90fc6e..3aff157 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/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

--
Gitblit v1.7.1