| | |
| | | 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; |
| | |
| | | this.tests = tests; |
| | | } |
| | | |
| | | /** 查询测试当前有效的检测柜与采集器实时观测,并补充可筛选的工步上下文。 */ |
| | | @Override public List<Map<String, Object>> records(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedRecords(testId); |
| | | List<Map<String, Object>> source = new java.util.ArrayList<>(); |
| | | List<Map<String, Object>> snapshots = mapper.selectPersistedRealtimeSnapshots(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 effective; |
| | | } |
| | | |
| | | @Override public List<Map<String, Object>> steps(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedSteps(testId); |
| | | return applyEffectiveData(testId, "STEP", mapper.selectPersistedSteps(testId)); |
| | | } |
| | | |
| | | @Override public List<Map<String, Object>> cycles(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedCycles(testId); |
| | | return applyEffectiveData(testId, "CYCLE", mapper.selectPersistedCycles(testId)); |
| | | } |
| | | |
| | | /** 将最新人工覆盖应用到设备事实,删除只影响当前有效视图,不抹除原始采样。 */ |
| | | 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) |