实现测试工步设置与工步汇总
新增测试级逻辑工步映射持久化、校验和审计接口,扩展检测柜工步统计指标,并在控制台按电池号和循环展示可重映射工步数据。
| | |
| | | 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。 */ |
| | |
| | | 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}") |
| New file |
| | |
| | | ALTER TABLE t_test ADD COLUMN logic_step_mappings_json JSON NULL AFTER remark; |
| | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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) { } |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | 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; } |
| | |
| | | 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; } |
| | | } |
| New file |
| | |
| | | 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; } |
| | | } |
| | |
| | | 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; |
| | |
| | | /** 按来源和起始边界顺序返回测试接收区间。 */ |
| | | 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); |
| | |
| | | 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 |
| | |
| | | |
| | | /** 查询指定测试已持久化的结果记录。 */ |
| | | List<TTestResult> selectTestResults(TTestResult result); |
| | | /** 保存当前测试的逻辑工步合并规则。 */ |
| | | int saveLogicStepMappings(Long testId, List<TTestLogicStepMapping> mappings, Long operatorId, String updateBy); |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | 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(); |
| | |
| | | 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) |
| | |
| | | <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> |
| | |
| | | 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 |
| | |
| | | <if test="endResultTime != null">and result.result_time <= #{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 |