package com.ruoyi.system.service.impl;
|
|
import java.util.List;
|
import java.util.ArrayList;
|
import java.util.Comparator;
|
import java.util.Date;
|
import java.time.Instant;
|
import java.time.OffsetDateTime;
|
import java.time.LocalDateTime;
|
import java.time.ZoneId;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
import java.util.HashMap;
|
import com.alibaba.fastjson2.JSON;
|
import org.springframework.stereotype.Service;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.system.domain.TTestDataOverride;
|
import com.ruoyi.system.mapper.TTestMapper;
|
import com.ruoyi.system.service.ITTestService;
|
import com.ruoyi.system.service.ITestDataQueryService;
|
|
/** 测试数据查询模块;实现不访问设备或模拟器历史。 */
|
@Service
|
public class TestDataQueryServiceImpl implements ITestDataQueryService
|
{
|
private final TTestMapper mapper;
|
private final ITTestService tests;
|
|
public TestDataQueryServiceImpl(TTestMapper mapper, ITTestService tests)
|
{
|
this.mapper = mapper;
|
this.tests = tests;
|
}
|
|
/** 查询测试当前有效的检测柜与采集器实时观测,并补充可筛选的工步上下文。 */
|
@Override public List<Map<String, Object>> records(Long testId)
|
{
|
requireTest(testId);
|
List<Map<String, Object>> source = new java.util.ArrayList<>();
|
List<Map<String, Object>> snapshots = mapper.selectPersistedFinalRealtimeRecords(testId);
|
if (snapshots != null && !snapshots.isEmpty()) source.addAll(snapshots);
|
else {
|
// 迁移完成前兼容旧数据;新采集只从统一快照读取。
|
List<Map<String, Object>> cabinet = mapper.selectPersistedRecords(testId);
|
List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
|
if (cabinet != null) source.addAll(cabinet);
|
if (collector != null) source.addAll(collector);
|
}
|
List<Map<String, Object>> effective = applyEffectiveData(testId, "REALTIME", source);
|
attachCollectorStepContext(effective);
|
return normalizeApiTimes(effective);
|
}
|
|
@Override public List<Map<String, Object>> steps(Long testId)
|
{
|
requireTest(testId);
|
List<Map<String, Object>> source = mapper.selectPersistedFinalStepRecords(testId);
|
if (source == null || source.isEmpty()) {
|
List<Map<String, Object>> legacy = mapper.selectPersistedSteps(testId);
|
if (legacy != null && !legacy.isEmpty()) source = legacy;
|
}
|
return normalizeApiTimes(applyEffectiveData(testId, "STEP", source));
|
}
|
|
@Override public List<Map<String, Object>> cycles(Long testId)
|
{
|
requireTest(testId);
|
List<Map<String, Object>> source = mapper.selectPersistedFinalCycleRecords(testId);
|
if (source == null || source.isEmpty()) {
|
List<Map<String, Object>> legacy = mapper.selectPersistedCycles(testId);
|
if (legacy != null && !legacy.isEmpty()) source = legacy;
|
}
|
return normalizeApiTimes(applyEffectiveData(testId, "CYCLE", source));
|
}
|
|
/** 自研业务接口统一以 Unix epoch 毫秒返回时间,避免前端依赖数据库日期格式。 */
|
private List<Map<String, Object>> normalizeApiTimes(List<Map<String, Object>> rows)
|
{
|
if (rows == null) return List.of();
|
for (Map<String, Object> row : rows)
|
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());
|
else if (value instanceof Instant instant) row.put(key, instant.toEpochMilli());
|
}
|
return rows;
|
}
|
|
/** 合并既有导入记录和人工修订供当前有效数据查询;不在此处执行物理删除。 */
|
private List<Map<String, Object>> applyEffectiveData(Long testId, String dataType,
|
List<Map<String, Object>> source)
|
{
|
List<Map<String, Object>> effective = new java.util.ArrayList<>(
|
source == null ? List.<Map<String, Object>>of() : source);
|
List<Map<String, Object>> imports = mapper.selectConsoleImportRecords(testId, dataType);
|
for (Map<String, Object> imported : imports == null ? List.<Map<String, Object>>of() : imports)
|
{
|
try
|
{
|
@SuppressWarnings("unchecked")
|
Map<String, Object> row = (Map<String, Object>) (Map<?, ?>) JSON.parseObject(
|
String.valueOf(imported.get("rowValues")), Map.class);
|
row.put("stableIdentity", imported.get("stableIdentity"));
|
effective.add(row);
|
}
|
catch (RuntimeException ignored) { }
|
}
|
List<TTestDataOverride> overrides = mapper.selectDataOverrides(testId, dataType);
|
if (effective.isEmpty() || overrides == null || overrides.isEmpty()) return effective;
|
Map<String, TTestDataOverride> byIdentity = new HashMap<>();
|
for (TTestDataOverride override : overrides)
|
if (override != null && override.getStableIdentity() != null)
|
byIdentity.put(override.getStableIdentity(), override);
|
List<Map<String, Object>> filtered = new java.util.ArrayList<>(effective.size());
|
for (Map<String, Object> row : effective)
|
{
|
String identity = row.get("stableIdentity") == null ? stableIdentity(dataType, row)
|
: String.valueOf(row.get("stableIdentity"));
|
TTestDataOverride override = byIdentity.get(identity);
|
if (override != null && Boolean.TRUE.equals(override.getDeleted())) continue;
|
Map<String, Object> copy = new LinkedHashMap<>(row);
|
if (override != null && override.getFieldValues() != null
|
&& !override.getFieldValues().isBlank())
|
{
|
try
|
{
|
Map<String, Object> values = JSON.parseObject(override.getFieldValues(), Map.class);
|
if (values != null) copy.putAll(values);
|
}
|
catch (RuntimeException ignored)
|
{
|
// 覆盖已由修订服务校验;解析异常时保留原始事实,避免查询整体失败。
|
}
|
}
|
filtered.add(copy);
|
}
|
return filtered;
|
}
|
|
private String stableIdentity(String dataType, Map<String, Object> row)
|
{
|
if ("REALTIME".equals(dataType) && "SNAPSHOT".equals(String.valueOf(row.get("sourceType"))))
|
return String.valueOf(row.getOrDefault("stableIdentity", "snapshot:" + value(row, "snapshotId")));
|
if ("REALTIME".equals(dataType) && "COLLECTOR".equals(String.valueOf(row.get("sourceType"))))
|
return String.join(":", "collector", identityPart(row, "collectorId"),
|
identityPart(row, "channelCode"), value(row, "sequenceId"), identityPart(row, "metricCode"));
|
String adapterId = identityPart(row, "adapterId");
|
String devtype = identityPart(row, "devtype");
|
String deviceId = identityPart(row, "deviceId");
|
String subdevid = identityPart(row, "subdevid");
|
String channelId = identityPart(row, "channelId");
|
String cycleNo = value(row, "cycleNo");
|
String channelIdentity = String.join(":", adapterId, devtype, deviceId, subdevid, channelId);
|
if ("REALTIME".equals(dataType)) return channelIdentity + ":" + value(row, "sequenceId");
|
if ("STEP".equals(dataType)) return "step:" + channelIdentity + ":" + cycleNo + ":" + value(row, "stepNo");
|
return "cycle:" + channelIdentity + ":" + cycleNo;
|
}
|
|
/** 按检测柜实际工步的设备时间边界给采集器样本补充筛选上下文,不配对采样点。 */
|
private void attachCollectorStepContext(List<Map<String, Object>> rows)
|
{
|
Map<String, StepWindowBuilder> builders = new LinkedHashMap<>();
|
for (Map<String, Object> row : rows)
|
{
|
if ("COLLECTOR".equals(String.valueOf(row.get("sourceType")))) continue;
|
Instant time = toInstant(row.get("deviceTime"));
|
if (time == null || row.get("stepNo") == null) continue;
|
String key = String.join("|", text(row, "batteryNo"), text(row, "cycleNo"), text(row, "stepNo"));
|
StepWindowBuilder builder = builders.computeIfAbsent(key, ignored -> new StepWindowBuilder(row, time));
|
builder.start = builder.start.isAfter(time) ? time : builder.start;
|
builder.end = builder.end == null || builder.end.isBefore(time) ? time : builder.end;
|
}
|
Map<String, List<StepWindow>> windowsByBattery = new LinkedHashMap<>();
|
for (StepWindowBuilder builder : builders.values())
|
windowsByBattery.computeIfAbsent(builder.batteryNo, ignored -> new ArrayList<>()).add(builder.build());
|
for (List<StepWindow> batteryWindows : windowsByBattery.values())
|
{
|
batteryWindows.sort(Comparator.comparing(StepWindow::start));
|
for (int index = 0; index < batteryWindows.size(); index++)
|
{
|
StepWindow current = batteryWindows.get(index);
|
Instant nextStart = index + 1 < batteryWindows.size() ? batteryWindows.get(index + 1).start() : null;
|
batteryWindows.set(index, current.withNextStart(nextStart));
|
}
|
}
|
for (Map<String, Object> row : rows)
|
{
|
if (!"COLLECTOR".equals(String.valueOf(row.get("sourceType")))) continue;
|
Instant time = toInstant(row.get("deviceTime"));
|
if (time == null) continue;
|
List<StepWindow> batteryWindows = windowsByBattery.getOrDefault(text(row, "batteryNo"), List.of());
|
StepWindow match = batteryWindows.stream()
|
.filter(window -> !time.isBefore(window.start())
|
&& (window.nextStart() == null || time.isBefore(window.nextStart())))
|
.findFirst().orElse(null);
|
if (match == null) continue;
|
row.put("cycleNo", match.cycleNo());
|
row.put("stepNo", match.stepNo());
|
row.put("stepName", match.stepName());
|
row.put("actualStepLabel", match.stepName());
|
}
|
}
|
|
private String text(Map<String, Object> row, String key)
|
{
|
Object value = row.get(key);
|
return value == null ? "" : String.valueOf(value);
|
}
|
|
private Instant toInstant(Object value)
|
{
|
if (value instanceof Instant instant) return instant;
|
if (value instanceof Date date) return date.toInstant();
|
if (value == null) return null;
|
String text = String.valueOf(value);
|
try { return Instant.parse(text); }
|
catch (RuntimeException ignored) { }
|
try { return OffsetDateTime.parse(text).toInstant(); }
|
catch (RuntimeException ignored) { }
|
try { return LocalDateTime.parse(text).atZone(ZoneId.of("Asia/Hong_Kong")).toInstant(); }
|
catch (RuntimeException ignored) { return null; }
|
}
|
|
private static final class StepWindowBuilder
|
{
|
private final String batteryNo;
|
private final String cycleNo;
|
private final String stepNo;
|
private final String stepName;
|
private Instant start;
|
private Instant end;
|
|
private StepWindowBuilder(Map<String, Object> row, Instant time)
|
{
|
batteryNo = row.get("batteryNo") == null ? "" : String.valueOf(row.get("batteryNo"));
|
cycleNo = row.get("cycleNo") == null ? "" : String.valueOf(row.get("cycleNo"));
|
stepNo = row.get("stepNo") == null ? "" : String.valueOf(row.get("stepNo"));
|
stepName = row.get("stepName") == null ? "-" : String.valueOf(row.get("stepName"));
|
start = time;
|
end = time;
|
}
|
|
private StepWindow build()
|
{
|
return new StepWindow(batteryNo, cycleNo, stepNo, stepName, start, end, null);
|
}
|
}
|
|
private record StepWindow(String batteryNo, String cycleNo, String stepNo, String stepName,
|
Instant start, Instant end, Instant nextStart)
|
{
|
private StepWindow withNextStart(Instant value)
|
{
|
return new StepWindow(batteryNo, cycleNo, stepNo, stepName, start, end, value);
|
}
|
}
|
|
private String identityPart(Map<String, Object> row, String key)
|
{
|
Object value = row == null ? null : row.get(key);
|
return value == null ? "" : String.valueOf(value);
|
}
|
|
private String value(Map<String, Object> row, String key)
|
{
|
return identityPart(row, key);
|
}
|
|
private void requireTest(Long testId)
|
{
|
if (testId == null || tests.selectTestById(testId) == null)
|
throw new ServiceException("测试不存在");
|
}
|
}
|