yeg
2026-08-31 43c8a44f5651c52edb1cc823bbb120881f732e36
实现测试工步设置与工步汇总

新增测试级逻辑工步映射持久化、校验和审计接口,扩展检测柜工步统计指标,并在控制台按电池号和循环展示可重映射工步数据。
2个文件已添加
7个文件已修改
124 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/db/migration/V23__test_logic_step_mappings.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestLogicStepMapping.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -20,6 +20,7 @@
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.TTestLogicStepMapping;
import com.ruoyi.system.service.ITTestService;
/** 测试管理 Controller。 */
@@ -135,6 +136,17 @@
        return success(testService.selectTestById(testId));
    }
    /** 保存当前测试的逻辑工步合并规则,不改写检测柜原始工步。 */
    @PreAuthorize("@ss.hasPermi('system:test:edit')")
    @Log(title = "测试工步设置", businessType = BusinessType.UPDATE)
    @PutMapping("/{testId}/logic-step-mappings")
    public AjaxResult saveLogicStepMappings(@PathVariable Long testId,
            @RequestBody List<TTestLogicStepMapping> mappings)
    {
        testService.saveLogicStepMappings(testId, mappings, getUserId(), getUsername());
        return success(testService.selectTestById(testId));
    }
    @PreAuthorize("@ss.hasPermi('system:test:remove')")
    @Log(title = "测试管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{testIds}")
ruoyi-admin/src/main/resources/db/migration/V23__test_logic_step_mappings.sql
New file
@@ -0,0 +1 @@
ALTER TABLE t_test ADD COLUMN logic_step_mappings_json JSON NULL AFTER remark;
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -417,10 +417,14 @@
        return grouped.entrySet().stream().map(entry -> {
            BtsCycleRecord calculated = entry.getValue().finish();
            BtsExcelFixture.CycleSummary expected = BtsExcelFixture.cycle(entry.getKey());
            if (expected == null || entry.getValue().count < expected.recordCount()) return calculated;
            return new BtsCycleRecord(calculated.cycleNo(), expected.recordCount(), calculated.startedAt(),
            String recordNo = "CYCLE-" + deviceId + "-" + channelId + "-" + entry.getKey();
            if (expected == null || entry.getValue().count < expected.recordCount())
                return new BtsCycleRecord(recordNo, calculated.cycleNo(), calculated.recordCount(), calculated.startedAt(), calculated.endedAt(),
                        calculated.chargeCapacityAh(), calculated.dischargeCapacityAh(), calculated.chargeEnergyWh(), calculated.dischargeEnergyWh(), calculated.efficiency());
            return new BtsCycleRecord(recordNo, calculated.cycleNo(), expected.recordCount(), calculated.startedAt(),
                    calculated.endedAt(), expected.chargeCapacityAh(), expected.dischargeCapacityAh(),
                    expected.chargeEnergyWh(), expected.dischargeEnergyWh());
                    expected.chargeEnergyWh(), expected.dischargeEnergyWh(),
                    efficiency(expected.chargeCapacityAh(), expected.dischargeCapacityAh()));
        }).toList();
    }
@@ -845,10 +849,17 @@
    }
    public record BtsStepRecord(int cycleNo, int stepNo, String stepName, String stepType, long recordCount,
            Instant startedAt, Instant endedAt, double capacityAh, double energyWh) { }
            Instant startedAt, Instant endedAt, double durationSeconds, double capacityAh, double energyWh,
            double startVoltageV, double endVoltageV, double chargePeakVoltageV, double dischargeValleyVoltageV) { }
    public record BtsCycleRecord(int cycleNo, long recordCount, Instant startedAt, Instant endedAt,
            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh) { }
    public record BtsCycleRecord(String recordNo, int cycleNo, long recordCount, Instant startedAt, Instant endedAt,
            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh,
            double efficiency) { }
    private static double efficiency(double chargeCapacityAh, double dischargeCapacityAh)
    {
        return chargeCapacityAh > 0d ? dischargeCapacityAh / chargeCapacityAh * 100d : 0d;
    }
    public record PlaybackSnapshot(int deviceId, int channelId, BtsPlaybackState playback,
            List<BtsHistoryRecord> records, List<BtsStepRecord> steps, List<BtsCycleRecord> cycles) { }
@@ -856,10 +867,11 @@
    private static final class BtsStepAccumulator
    {
        private final int cycle; private final int step; private final String name; private final String type;
        private long count; private Instant first; private Instant last; private double capacity; private double energy;
        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(); }
        void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); capacity = item.capacityAh(); energy = item.energyWh(); }
        BtsStepRecord finish() { return new BtsStepRecord(cycle, step, name, type, count, first, last, capacity, energy); }
        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); }
    }
    private static final class BtsCycleAccumulator
@@ -869,7 +881,7 @@
        private double chargeEnergy; private double dischargeEnergy;
        BtsCycleAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); }
        void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); maximumCapacity = Math.max(maximumCapacity, item.capacityAh()); minimumCapacity = Math.min(minimumCapacity, item.capacityAh()); if (item.currentA() >= 0) chargeEnergy = Math.max(chargeEnergy, item.energyWh()); else dischargeEnergy = Math.max(dischargeEnergy, Math.abs(item.energyWh())); }
        BtsCycleRecord finish() { return new BtsCycleRecord(cycle, count, first, last, Math.max(0d, maximumCapacity), Math.max(0d, maximumCapacity - minimumCapacity), chargeEnergy, dischargeEnergy); }
        BtsCycleRecord finish() { double charge = Math.max(0d, maximumCapacity); double discharge = Math.max(0d, maximumCapacity - minimumCapacity); return new BtsCycleRecord(cycle, count, first, last, charge, discharge, chargeEnergy, dischargeEnergy, efficiency(charge, discharge)); }
    }
    private java.util.Set<String> requestedChannels(String request)
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTest.java
@@ -60,6 +60,8 @@
    private List<TTestFieldSnapshot> fieldSnapshots;
    private List<TTestReceptionInterval> receptionIntervals;
    private List<TTestResult> resultSummary;
    private String logicStepMappingsJson;
    private List<TTestLogicStepMapping> logicStepMappings;
    public Long getTestId() { return testId; }
    public void setTestId(Long value) { testId = value; }
@@ -121,4 +123,8 @@
    public void setReceptionIntervals(List<TTestReceptionInterval> value) { receptionIntervals = value; }
    public List<TTestResult> getResultSummary() { return resultSummary; }
    public void setResultSummary(List<TTestResult> value) { resultSummary = value; }
    public String getLogicStepMappingsJson() { return logicStepMappingsJson; }
    public void setLogicStepMappingsJson(String value) { logicStepMappingsJson = value; }
    public List<TTestLogicStepMapping> getLogicStepMappings() { return logicStepMappings; }
    public void setLogicStepMappings(List<TTestLogicStepMapping> value) { logicStepMappings = value; }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestLogicStepMapping.java
New file
@@ -0,0 +1,22 @@
package com.ruoyi.system.domain;
import java.io.Serializable;
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;
    public Integer getLogicalNo() { return logicalNo; }
    public void setLogicalNo(Integer value) { logicalNo = value; }
    public String getLogicalName() { return logicalName; }
    public void setLogicalName(String value) { logicalName = value; }
    public Integer getStartStepNo() { return startStepNo; }
    public void setStartStepNo(Integer value) { startStepNo = value; }
    public Integer getEndStepNo() { return endStepNo; }
    public void setEndStepNo(Integer value) { endStepNo = value; }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -14,6 +14,7 @@
import com.ruoyi.system.domain.TTestCollectorSource;
import com.ruoyi.system.domain.TTestCollectorExceptionInterval;
import com.ruoyi.system.domain.TTestReceptionInterval;
import com.ruoyi.system.domain.TTestLogicStepMapping;
import com.ruoyi.system.domain.TTestBackfillCursor;
import com.ruoyi.system.domain.TTestIntegrityIssue;
import com.ruoyi.system.domain.TDetectionChannel;
@@ -55,6 +56,9 @@
    /** 按来源和起始边界顺序返回测试接收区间。 */
    List<TTestReceptionInterval> selectTestReceptionIntervals(Long testId);
    List<TTestResult> selectTestResults(TTestResult result);
    String selectLogicStepMappingsJson(Long testId);
    int updateLogicStepMappings(@Param("testId") Long testId, @Param("mappingsJson") String mappingsJson,
            @Param("updateBy") String updateBy, @Param("updateTime") Date updateTime);
    int countTestTypeEnabled(Long testTypeId);
    int countUserEnabled(Long userId);
    int countBatteryInEnabledGroup(@Param("groupId") Long groupId, @Param("batteryNo") String batteryNo);
ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
@@ -4,6 +4,7 @@
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
import com.ruoyi.system.domain.TTestLogicStepMapping;
/** 测试配置业务接口。 */
public interface ITTestService
@@ -43,5 +44,7 @@
    /** 查询指定测试已持久化的结果记录。 */
    List<TTestResult> selectTestResults(TTestResult result);
    /** 保存当前测试的逻辑工步合并规则。 */
    int saveLogicStepMappings(Long testId, List<TTestLogicStepMapping> mappings, Long operatorId, String updateBy);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -25,6 +25,7 @@
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.TTestFieldSnapshot;
import com.ruoyi.system.domain.TTestWarningThreshold;
import com.ruoyi.system.domain.TTestLogicStepMapping;
import com.ruoyi.system.domain.TDetectionChannel;
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
import com.ruoyi.system.mapper.TTestMapper;
@@ -333,6 +334,35 @@
        return testMapper.selectTestResults(result);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int saveLogicStepMappings(Long testId, List<TTestLogicStepMapping> mappings, Long operatorId, String updateBy)
    {
        if (testId == null || operatorId == null) throw new ServiceException("测试和操作人不能为空");
        TTest current = testMapper.selectTestByIdForUpdate(testId);
        if (current == null) throw new ServiceException("测试不存在");
        List<TTestLogicStepMapping> normalized = mappings == null ? List.of() : mappings;
        int previousEnd = 0;
        int expectedNo = 1;
        for (TTestLogicStepMapping mapping : normalized)
        {
            if (mapping == null || mapping.getLogicalNo() == null || mapping.getLogicalNo() != expectedNo
                    || mapping.getStartStepNo() == null || mapping.getEndStepNo() == null
                    || mapping.getStartStepNo() < 1 || mapping.getEndStepNo() < mapping.getStartStepNo()
                    || mapping.getStartStepNo() <= previousEnd || StringUtils.isEmpty(trimToNull(mapping.getLogicalName())))
                throw new ServiceException("逻辑工步必须按连续实际工步升序且互不重叠");
            mapping.setLogicalName(trimToNull(mapping.getLogicalName()));
            previousEnd = mapping.getEndStepNo();
            expectedNo++;
        }
        String oldJson = current.getLogicStepMappingsJson();
        String newJson = JSON.toJSONString(normalized);
        int rows = testMapper.updateLogicStepMappings(testId, newJson, updateBy, new Date());
        if (rows != 1) throw new ServiceException("工步设置已变更,请刷新后重试");
        testMapper.insertTestAudit(testId, "LOGIC_STEP_MAPPING_UPDATED", operatorId, updateBy, oldJson, newJson);
        return rows;
    }
    private void loadConfiguration(TTest test)
    {
        Long testId = test.getTestId();
@@ -385,6 +415,10 @@
        test.setReceptionIntervals(intervals == null ? List.of() : intervals);
        List<TTestFieldSnapshot> loadedSnapshots = sharedFieldSnapshotService.selectByTestId(testId);
        test.setFieldSnapshots(loadedSnapshots == null ? mutable(test.getFieldSnapshots()) : loadedSnapshots);
        String mappingJson = test.getLogicStepMappingsJson();
        try { test.setLogicStepMappings(mappingJson == null || mappingJson.isBlank()
                ? new ArrayList<>() : JSON.parseArray(mappingJson, TTestLogicStepMapping.class)); }
        catch (JSONException exception) { throw new ServiceException("逻辑工步设置无法解析"); }
    }
    private void validateBase(TTest test, boolean update)
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -28,6 +28,7 @@
        <result property="updateBy" column="update_by" />
        <result property="updateTime" column="update_time" />
        <result property="remark" column="remark" />
        <result property="logicStepMappingsJson" column="logic_step_mappings_json" />
        <result property="batteryCount" column="battery_count" />
        <result property="batteryNo" column="battery_no" />
    </resultMap>
@@ -157,6 +158,7 @@
               t.reception_status, t.monitor_timeout_minutes,
               t.started_at_utc, t.ended_at_utc, t.del_flag,
               t.create_by, t.create_time, t.update_by, t.update_time, t.remark,
               cast(t.logic_step_mappings_json as char) logic_step_mappings_json,
               (select count(1) from t_test_battery b where b.test_id = t.test_id and b.del_flag = 0) battery_count,
               t.runtime_attach runtime_attach, t.backfill_status, t.backfill_started_at_utc,
               t.backfill_completed_at_utc
@@ -352,6 +354,14 @@
        <if test="endResultTime != null">and result.result_time &lt;= #{endResultTime}</if>
        order by result.result_time, result.result_id
    </select>
    <select id="selectLogicStepMappingsJson" resultType="string">
        select cast(logic_step_mappings_json as char) from t_test where test_id = #{testId} and del_flag = 0
    </select>
    <update id="updateLogicStepMappings">
        update t_test set logic_step_mappings_json = cast(#{mappingsJson} as json),
            update_by = #{updateBy}, update_time = #{updateTime}
        where test_id = #{testId} and del_flag = 0
    </update>
    <select id="countTestTypeEnabled" resultType="int">
        select count(1) from t_test_type where test_type_id = #{testTypeId} and status = '0' and del_flag = 0