yeg
2026-09-01 d1cb58bebb89c000275d9b092dd644c809296707
修复未开始测试继承设备历史

未开始接收时固定返回第一循环待执行方案,避免复用检测柜通道播放进度,并补充循环位置契约和回归测试。
6个文件已修改
69 ■■■■■ 已修改文件
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsDevelopmentSimulatorRecordStepCycleTest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestLogicStepMapping.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -403,7 +403,10 @@
        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();
    }
@@ -430,11 +433,15 @@
    /** 返回检测柜测试方案事实;方案行来自 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();
@@ -450,24 +457,25 @@
            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);
    }
@@ -891,7 +899,7 @@
        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) { }
@@ -899,7 +907,7 @@
            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,
@@ -915,12 +923,12 @@
    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
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsDevelopmentSimulatorRecordStepCycleTest.java
@@ -65,7 +65,10 @@
                    .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();
@@ -75,6 +78,22 @@
    }
    @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");
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestLogicStepMapping.java
@@ -4,13 +4,13 @@
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; }
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java
@@ -29,7 +29,7 @@
    }
    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) { }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
@@ -106,9 +106,11 @@
                    || 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(),
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -350,7 +350,7 @@
                    || 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++;