对齐控制台时间与扩展测量响应
测试详情、修订与实时流统一输出毫秒时间,补充采集器设备时间和扩展测量字段,并验证业务序列化边界。
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | 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; |
| | |
| | | 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; } |
| | |
| | | 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; |
| | |
| | | |
| | | /** 打开实时 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) { } |
| | |
| | | } |
| | | 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) |
| | |
| | | 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()); |
| | |
| | | { |
| | | 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()); |
| | | } |
| | |
| | | { |
| | | 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()); |
| | |
| | | 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 |