修复未开始测试继承设备历史
未开始接收时固定返回第一循环待执行方案,避免复用检测柜通道播放进度,并补充循环位置契约和回归测试。
| | |
| | | for (BtsHistoryRecord item : btsRecords(deviceId, channelId)) |
| | | { |
| | | String key = item.cycleNo() + ":" + item.stepNo(); |
| | | grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item)).add(item); |
| | | int position = BtsExcelFixture.records().stream() |
| | | .filter(record -> record.cycleNo() == item.cycleNo()) |
| | | .map(BtsExcelFixture.FixtureRecord::stepNo).distinct().toList().indexOf(item.stepNo()) + 1; |
| | | grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item, position)).add(item); |
| | | } |
| | | return grouped.values().stream().map(BtsStepAccumulator::finish).toList(); |
| | | } |
| | |
| | | |
| | | /** 返回检测柜测试方案事实;方案行来自 Excel 夹具,当前状态来自该通道播放状态。 */ |
| | | public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId) |
| | | { return btsTest(deviceId, channelId, null); } |
| | | |
| | | /** 返回指定循环的检测柜测试方案;未开始接收的测试固定使用第一个循环作为核对上下文。 */ |
| | | public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId, Integer forcedCycleNo) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now()); |
| | | int requestedCycleNo = playback.cycleNo(); |
| | | int requestedCycleNo = forcedCycleNo == null ? playback.cycleNo() : forcedCycleNo; |
| | | int currentCycleNo = requestedCycleNo; |
| | | List<BtsExcelFixture.FixtureRecord> currentCycleRecords = BtsExcelFixture.records().stream() |
| | | .filter(item -> item.cycleNo() == requestedCycleNo).toList(); |
| | |
| | | firstByStep.putIfAbsent(item.stepNo(), item); |
| | | |
| | | List<BtsTestPlanRow> rows = new ArrayList<>(); |
| | | int cyclePosition = 1; |
| | | for (BtsExcelFixture.FixtureRecord first : firstByStep.values()) |
| | | { |
| | | List<BtsExcelFixture.FixtureRecord> sameCycle = currentCycleRecords.stream() |
| | | .filter(item -> item.stepNo() == first.stepNo()).toList(); |
| | | BtsExcelFixture.FixtureRecord last = sameCycle.get(sameCycle.size() - 1); |
| | | // step 号在每个循环内重复,跨循环时必须按当前循环的工步位置判断,不能把上一循环的同号工步当成当前状态。 |
| | | String status = playback.naturallyFinished() ? "已完成" |
| | | String status = forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" |
| | | : playback.stepNo() > first.stepNo() ? "已完成" |
| | | : playback.stepNo() == first.stepNo() ? "进行中" : "待执行"; |
| | | rows.add(new BtsTestPlanRow("STEP", first.stepNo(), first.stepName(), |
| | | rows.add(new BtsTestPlanRow("STEP", first.stepNo(), cyclePosition++, first.stepName(), |
| | | Math.max(0d, last.offsetSeconds() - first.offsetSeconds()), first.voltageV(), status, |
| | | null, null)); |
| | | } |
| | | rows.add(new BtsTestPlanRow("CYCLE", BtsExcelFixture.startStepNo(), "循环", 0d, null, |
| | | playback.naturallyFinished() ? "已完成" : "进行中", currentCycleNo, |
| | | rows.add(new BtsTestPlanRow("CYCLE", BtsExcelFixture.startStepNo(), null, "循环", 0d, null, |
| | | forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "进行中", currentCycleNo, |
| | | (long) currentCycleRecords.size())); |
| | | rows.add(new BtsTestPlanRow("END", 0, "结束", 0d, null, |
| | | playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount())); |
| | | rows.add(new BtsTestPlanRow("END", 0, null, "结束", 0d, null, |
| | | forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount())); |
| | | return new BtsTestSnapshot(currentCycleNo, playback.stepNo(), BtsExcelFixture.cycleCount(), rows); |
| | | } |
| | | |
| | |
| | | stateStore.saveBtsPlaybackState(key, playback); |
| | | } |
| | | |
| | | public record BtsStepRecord(int cycleNo, int stepNo, String stepName, String stepType, long recordCount, |
| | | public record BtsStepRecord(int cycleNo, int stepNo, Integer cyclePosition, String stepName, String stepType, long recordCount, |
| | | Instant startedAt, Instant endedAt, double durationSeconds, double capacityAh, double energyWh, |
| | | double startVoltageV, double endVoltageV, double chargePeakVoltageV, double dischargeValleyVoltageV) { } |
| | | |
| | |
| | | double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh, |
| | | double efficiency) { } |
| | | |
| | | public record BtsTestPlanRow(String rowType, int stepNo, String stepName, double durationSeconds, |
| | | public record BtsTestPlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds, |
| | | Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { } |
| | | |
| | | public record BtsTestSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount, |
| | |
| | | |
| | | private static final class BtsStepAccumulator |
| | | { |
| | | private final int cycle; private final int step; private final String name; private final String type; |
| | | private final int cycle; private final int step; private final int position; private final String name; private final String type; |
| | | private long count; private Instant first; private Instant last; private double duration; private double capacity; private double energy; |
| | | private double startVoltage; private double endVoltage; private double chargePeak = Double.NEGATIVE_INFINITY; private double dischargeValley = Double.POSITIVE_INFINITY; |
| | | BtsStepAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); step = first.stepNo(); name = first.stepName(); type = first.stepType(); } |
| | | BtsStepAccumulator(BtsHistoryRecord first, int position) { cycle = first.cycleNo(); step = first.stepNo(); this.position = position; name = first.stepName(); type = first.stepType(); } |
| | | void add(BtsHistoryRecord item) { count++; if (first == null) { first = item.deviceTime(); startVoltage = item.voltageV(); } last = item.deviceTime(); endVoltage = item.voltageV(); duration = first == null ? 0 : Math.max(0, java.time.Duration.between(first, last).toMillis() / 1000d); capacity = item.capacityAh(); energy = item.energyWh(); if (item.currentA() >= 0) chargePeak = Math.max(chargePeak, item.voltageV()); else dischargeValley = Math.min(dischargeValley, item.voltageV()); } |
| | | BtsStepRecord finish() { return new BtsStepRecord(cycle, step, name, type, count, first, last, duration, capacity, energy, startVoltage, endVoltage, chargePeak == Double.NEGATIVE_INFINITY ? 0 : chargePeak, dischargeValley == Double.POSITIVE_INFINITY ? 0 : dischargeValley); } |
| | | BtsStepRecord finish() { return new BtsStepRecord(cycle, step, position, name, type, count, first, last, duration, capacity, energy, startVoltage, endVoltage, chargePeak == Double.NEGATIVE_INFINITY ? 0 : chargePeak, dischargeValley == Double.POSITIVE_INFINITY ? 0 : dischargeValley); } |
| | | } |
| | | |
| | | private static final class BtsCycleAccumulator |
| | |
| | | .map(BtsExcelFixture.FixtureRecord::stepNo).distinct().toList(); |
| | | var actualSteps = snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) |
| | | .map(BtsDevelopmentSimulator.BtsTestPlanRow::stepNo).toList(); |
| | | var cyclePositions = snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) |
| | | .map(BtsDevelopmentSimulator.BtsTestPlanRow::cyclePosition).toList(); |
| | | assertEquals(expectedSteps, actualSteps); |
| | | assertEquals(java.util.stream.IntStream.rangeClosed(1, expectedSteps.size()).boxed().toList(), cyclePositions); |
| | | assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType()) && "进行中".equals(row.executionStatus()))); |
| | | assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType()) && "待执行".equals(row.executionStatus()))); |
| | | var cycleRow = snapshot.rows().stream().filter(row -> "CYCLE".equals(row.rowType())).findFirst().orElseThrow(); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void forcedFirstCycleKeepsUnstartedTestPlanIndependentOfSharedPlayback() throws Exception |
| | | { |
| | | InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); |
| | | try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) |
| | | { |
| | | simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); |
| | | simulator.nextBtsCycle(1, 1); |
| | | simulator.nextBtsCycle(1, 1); |
| | | var snapshot = simulator.btsTest(1, 1, 1); |
| | | assertEquals(1, snapshot.currentCycleNo()); |
| | | assertTrue(snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) |
| | | .allMatch(row -> "待执行".equals(row.executionStatus()))); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void playbackSpeedPauseResumeAndRestartAreIndependentOfDeviceStatus() throws Exception |
| | | { |
| | | Instant start = Instant.parse("2026-08-31T00:00:00Z"); |
| | |
| | | import jakarta.validation.constraints.Min; |
| | | import jakarta.validation.constraints.NotBlank; |
| | | |
| | | /** 当前测试的实际工步到逻辑工步映射;只影响展示和统计,不改写原始检测柜数据。 */ |
| | | /** 当前测试循环内工步位置到逻辑工步的映射;只影响展示和统计,不改写原始检测柜数据。 */ |
| | | public class TTestLogicStepMapping implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | @Min(value = 1, message = "逻辑工步编号必须大于0") private Integer logicalNo; |
| | | @NotBlank(message = "逻辑工步名称不能为空") private String logicalName; |
| | | @Min(value = 1, message = "实际工步起始编号必须大于0") private Integer startStepNo; |
| | | @Min(value = 1, message = "实际工步结束编号必须大于0") private Integer endStepNo; |
| | | @Min(value = 1, message = "循环内起始位置必须大于0") private Integer startStepNo; |
| | | @Min(value = 1, message = "循环内结束位置必须大于0") private Integer endStepNo; |
| | | public Integer getLogicalNo() { return logicalNo; } |
| | | public void setLogicalNo(Integer value) { logicalNo = value; } |
| | | public String getLogicalName() { return logicalName; } |
| | |
| | | } |
| | | record TestPlanSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount, |
| | | List<TestPlanRow> rows) { } |
| | | record TestPlanRow(String rowType, int stepNo, String stepName, double durationSeconds, |
| | | record TestPlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds, |
| | | Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { } |
| | | record LiveSource(String sourceType, Long sourceId, String batteryNo, String deviceStatus, |
| | | Map<String, Object> latestSnapshot) { } |
| | |
| | | || battery.getBinding().getChlid() == null) continue; |
| | | try |
| | | { |
| | | var source = simulator.btsTest(battery.getBinding().getDevid(), battery.getBinding().getChlid()); |
| | | var source = "NOT_STARTED".equals(test.getReceptionStatus()) |
| | | ? simulator.btsTest(battery.getBinding().getDevid(), battery.getBinding().getChlid(), 1) |
| | | : simulator.btsTest(battery.getBinding().getDevid(), battery.getBinding().getChlid()); |
| | | List<TestPlanRow> rows = source.rows().stream() |
| | | .map(row -> new TestPlanRow(row.rowType(), row.stepNo(), row.stepName(), row.durationSeconds(), |
| | | .map(row -> new TestPlanRow(row.rowType(), row.stepNo(), row.cyclePosition(), row.stepName(), row.durationSeconds(), |
| | | row.voltageV(), row.executionStatus(), row.cycleCount(), row.recordCount())) |
| | | .toList(); |
| | | return new TestPlanSnapshot(source.currentCycleNo(), source.currentStepNo(), |
| | |
| | | || mapping.getStartStepNo() == null || mapping.getEndStepNo() == null |
| | | || mapping.getStartStepNo() < 1 || mapping.getEndStepNo() < mapping.getStartStepNo() |
| | | || mapping.getStartStepNo() <= previousEnd || StringUtils.isEmpty(trimToNull(mapping.getLogicalName()))) |
| | | throw new ServiceException("逻辑工步必须按连续实际工步升序且互不重叠"); |
| | | throw new ServiceException("逻辑工步必须按循环内连续位置升序且互不重叠"); |
| | | mapping.setLogicalName(trimToNull(mapping.getLogicalName())); |
| | | previousEnd = mapping.getEndStepNo(); |
| | | expectedNo++; |