实现测试预警异常管理
新增成功接收数据的阈值异常聚合、查询与负责人关闭审计。完善异常表开放记录约束和模拟器专项测试。
| New file |
| | |
| | | package com.ruoyi.web.controller.system; |
| | | |
| | | import java.util.List; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import com.ruoyi.common.annotation.Log; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import com.ruoyi.common.core.page.TableDataInfo; |
| | | import com.ruoyi.common.enums.BusinessType; |
| | | import com.ruoyi.system.domain.TTestWarningException; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.service.impl.TestWarningExceptionService; |
| | | |
| | | /** 测试预警异常查询与人工关闭接口。 */ |
| | | @RestController |
| | | @RequestMapping("/system/test-warning-exceptions") |
| | | public class TTestWarningExceptionController extends BaseController { |
| | | @Autowired private TTestMapper mapper; |
| | | @Autowired private ITTestService testService; |
| | | @Autowired private TestWarningExceptionService warningExceptionService; |
| | | /** 按测试和状态查询后端异常事实。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:exception:query')") |
| | | @GetMapping("/list") |
| | | public TableDataInfo list(@RequestParam(required=false) Long testId, @RequestParam(required=false) String status) { |
| | | startPage(); |
| | | List<TTestWarningException> rows = mapper.selectWarningExceptions(testId, status); |
| | | return getDataTable(rows); |
| | | } |
| | | /** 校验测试负责人身份并关闭异常,同时追加测试审计事实。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:exception:handle')") |
| | | @Log(title = "测试预警异常", businessType = BusinessType.UPDATE) |
| | | @PutMapping("/{exceptionId}/close") |
| | | public AjaxResult close(@PathVariable Long exceptionId, @RequestBody CloseRequest request) { |
| | | if (request == null || request.handlingNote() == null || request.handlingNote().trim().isEmpty()) return error("处理结果不能为空"); |
| | | TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId); |
| | | if (exception == null) return error("异常不存在"); |
| | | var test = testService.selectTestById(exception.getTestId()); |
| | | if (test == null || !getUserId().equals(test.getPrincipalId())) |
| | | return error("仅测试负责人可以关闭预警异常"); |
| | | int rows = warningExceptionService.close(exceptionId, getUserId(), getUsername(), request.handlingNote().trim()); |
| | | return rows == 1 ? success(mapper.selectWarningExceptionById(exceptionId)) : error("异常已关闭或不存在"); |
| | | } |
| | | public record CloseRequest(String handlingNote) {} |
| | | } |
| New file |
| | |
| | | CREATE TABLE t_test_warning_exception ( |
| | | exception_id BIGINT NOT NULL AUTO_INCREMENT, |
| | | test_id BIGINT NOT NULL, |
| | | threshold_id BIGINT NOT NULL, |
| | | source_type VARCHAR(32) NOT NULL, |
| | | source_identity VARCHAR(255) NOT NULL DEFAULT '', |
| | | metric_code VARCHAR(100) NOT NULL, |
| | | abnormal_value DECIMAL(30,12) NOT NULL, |
| | | unit VARCHAR(32) NOT NULL, |
| | | rule_snapshot JSON NOT NULL, |
| | | severity VARCHAR(32) NOT NULL DEFAULT 'WARNING', |
| | | exception_status VARCHAR(16) NOT NULL DEFAULT 'OPEN', |
| | | first_occurred_at_utc DATETIME(3) NOT NULL, |
| | | last_occurred_at_utc DATETIME(3) NOT NULL, |
| | | occurrence_count BIGINT NOT NULL DEFAULT 1, |
| | | handled_by BIGINT NULL, |
| | | handled_at_utc DATETIME(3) NULL, |
| | | handling_note VARCHAR(1000) NULL, |
| | | PRIMARY KEY (exception_id), |
| | | open_threshold_id BIGINT GENERATED ALWAYS AS (IF(exception_status = 'OPEN', threshold_id, NULL)) STORED, |
| | | UNIQUE KEY uk_warning_exception_open (test_id, metric_code, open_threshold_id), |
| | | KEY idx_warning_exception_query (test_id, exception_status, last_occurred_at_utc) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='测试预警阈值异常'; |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import static org.mockito.ArgumentMatchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import org.junit.jupiter.api.Test; |
| | | import com.ruoyi.system.domain.TTest; |
| | | import com.ruoyi.system.domain.TTestWarningException; |
| | | import com.ruoyi.system.domain.TTestWarningThreshold; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | |
| | | /** 测试预警仅由成功接收测量值触发,并通过持久层原子聚合。 */ |
| | | class TestWarningExceptionServiceTest { |
| | | @Test |
| | | void evaluatesOnlyOutOfRangeValues() { |
| | | TTestMapper mapper = mock(TTestMapper.class); |
| | | TestWarningExceptionService service = new TestWarningExceptionService(mapper); |
| | | TTest test = testWithThreshold("TEMPERATURE", new BigDecimal("0"), new BigDecimal("30")); |
| | | |
| | | service.evaluate(test, "COLLECTOR", "adapter:9:S-01", "temperature", new BigDecimal("31"), "℃", new Date()); |
| | | service.evaluate(test, "COLLECTOR", "adapter:9:S-01", "temperature", new BigDecimal("20"), "℃", new Date()); |
| | | |
| | | verify(mapper, times(1)).upsertWarningException(eq(1L), eq(11L), eq("COLLECTOR"), eq("adapter:9:S-01"), |
| | | eq("temperature"), eq(new BigDecimal("31")), eq("℃"), anyString(), eq("WARNING"), any(Date.class)); |
| | | } |
| | | |
| | | @Test |
| | | void closesOpenExceptionAndAppendsAuditInServiceOperation() { |
| | | TTestMapper mapper = mock(TTestMapper.class); |
| | | TestWarningExceptionService service = new TestWarningExceptionService(mapper); |
| | | TTestWarningException exception = new TTestWarningException(); |
| | | exception.setExceptionId(7L); |
| | | exception.setTestId(1L); |
| | | when(mapper.selectWarningExceptionById(7L)).thenReturn(exception); |
| | | when(mapper.closeWarningException(eq(7L), eq(22L), eq("负责人"), any(Date.class), eq("已核实"))).thenReturn(1); |
| | | |
| | | int rows = service.close(7L, 22L, "负责人", "已核实"); |
| | | |
| | | assertEquals(1, rows); |
| | | verify(mapper).insertTestAudit(eq(1L), eq("WARNING_EXCEPTION_CLOSED"), eq(22L), eq("负责人"), isNull(), contains("已核实")); |
| | | } |
| | | |
| | | private static TTest testWithThreshold(String metric, BigDecimal min, BigDecimal max) { |
| | | TTestWarningThreshold threshold = new TTestWarningThreshold(); |
| | | threshold.setThresholdId(11L); |
| | | threshold.setMetricCode(metric); |
| | | threshold.setMinValue(min); |
| | | threshold.setMaxValue(max); |
| | | threshold.setUnit("℃"); |
| | | threshold.setSeverity("WARNING"); |
| | | TTest test = new TTest(); |
| | | test.setTestId(1L); |
| | | test.setThresholds(List.of(threshold)); |
| | | return test; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.domain; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | import java.util.Date; |
| | | |
| | | /** 测试阈值预警异常事实。 */ |
| | | public class TTestWarningException implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | private Long exceptionId; |
| | | private Long testId; |
| | | private String testName; |
| | | private String sourceType; |
| | | private String sourceIdentity; |
| | | private String metricCode; |
| | | private BigDecimal abnormalValue; |
| | | private String unit; |
| | | private String ruleSnapshot; |
| | | private String severity; |
| | | private String exceptionStatus; |
| | | private Date firstOccurredAtUtc; |
| | | private Date lastOccurredAtUtc; |
| | | private Long occurrenceCount; |
| | | private Long handledBy; |
| | | private String handledByName; |
| | | private Date handledAtUtc; |
| | | private String handlingNote; |
| | | public Long getExceptionId(){return exceptionId;} public void setExceptionId(Long v){exceptionId=v;} |
| | | public Long getTestId(){return testId;} public void setTestId(Long v){testId=v;} |
| | | public String getTestName(){return testName;} public void setTestName(String v){testName=v;} |
| | | public String getSourceType(){return sourceType;} public void setSourceType(String v){sourceType=v;} |
| | | public String getSourceIdentity(){return sourceIdentity;} public void setSourceIdentity(String v){sourceIdentity=v;} |
| | | public String getMetricCode(){return metricCode;} public void setMetricCode(String v){metricCode=v;} |
| | | public BigDecimal getAbnormalValue(){return abnormalValue;} public void setAbnormalValue(BigDecimal v){abnormalValue=v;} |
| | | public String getUnit(){return unit;} public void setUnit(String v){unit=v;} |
| | | public String getRuleSnapshot(){return ruleSnapshot;} public void setRuleSnapshot(String v){ruleSnapshot=v;} |
| | | public String getSeverity(){return severity;} public void setSeverity(String v){severity=v;} |
| | | public String getExceptionStatus(){return exceptionStatus;} public void setExceptionStatus(String v){exceptionStatus=v;} |
| | | public Date getFirstOccurredAtUtc(){return firstOccurredAtUtc;} public void setFirstOccurredAtUtc(Date v){firstOccurredAtUtc=v;} |
| | | public Date getLastOccurredAtUtc(){return lastOccurredAtUtc;} public void setLastOccurredAtUtc(Date v){lastOccurredAtUtc=v;} |
| | | public Long getOccurrenceCount(){return occurrenceCount;} public void setOccurrenceCount(Long v){occurrenceCount=v;} |
| | | public Long getHandledBy(){return handledBy;} public void setHandledBy(Long v){handledBy=v;} |
| | | public String getHandledByName(){return handledByName;} public void setHandledByName(String v){handledByName=v;} |
| | | public Date getHandledAtUtc(){return handledAtUtc;} public void setHandledAtUtc(Date v){handledAtUtc=v;} |
| | | public String getHandlingNote(){return handlingNote;} public void setHandlingNote(String v){handlingNote=v;} |
| | | } |
| | |
| | | import com.ruoyi.system.domain.TTestPlanStep; |
| | | import com.ruoyi.system.domain.TTestResult; |
| | | import com.ruoyi.system.domain.TTestWarningThreshold; |
| | | import com.ruoyi.system.domain.TTestWarningException; |
| | | import com.ruoyi.system.domain.TTestLogicStepMapping; |
| | | import com.ruoyi.system.domain.TDetectionChannel; |
| | | import com.ruoyi.system.domain.TTestDataRevisionAction; |
| | |
| | | List<TTestChannelBinding> selectTestChannelBindings(Long testId); |
| | | List<TTestCollectorChannel> selectTestCollectorChannels(Long testId); |
| | | List<TTestWarningThreshold> selectTestThresholds(Long testId); |
| | | /** 以测试、指标和阈值规则为开放异常键原子聚合成功接收的超限值。 */ |
| | | int upsertWarningException(@Param("testId") Long testId, @Param("thresholdId") Long thresholdId, |
| | | @Param("sourceType") String sourceType, @Param("sourceIdentity") String sourceIdentity, |
| | | @Param("metricCode") String metricCode, @Param("value") java.math.BigDecimal value, |
| | | @Param("unit") String unit, @Param("ruleSnapshot") String ruleSnapshot, |
| | | @Param("severity") String severity, @Param("occurredAt") Date occurredAt); |
| | | /** 查询异常事实;不从设备状态或缺失数据推导异常。 */ |
| | | List<TTestWarningException> selectWarningExceptions(@Param("testId") Long testId, |
| | | @Param("status") String status); |
| | | TTestWarningException selectWarningExceptionById(Long exceptionId); |
| | | /** 仅将开放异常转为已关闭,原始异常事实仍保留。 */ |
| | | int closeWarningException(@Param("exceptionId") Long exceptionId, @Param("handledBy") Long handledBy, |
| | | @Param("handledByName") String handledByName, @Param("handledAt") Date handledAt, |
| | | @Param("handlingNote") String handlingNote); |
| | | /** 查询当前正在接收的测试,供接收调度绑定测试事实。 */ |
| | | List<Long> selectReceivingTestIds(); |
| | | int insertRealtimeSnapshot(@Param("testId") Long testId, @Param("bindingId") Long bindingId, |
| | |
| | | import java.util.concurrent.TimeUnit; |
| | | import jakarta.annotation.PreDestroy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator.CollectorSnapshot; |
| | |
| | | import com.ruoyi.system.service.ITestLiveService.TestPlanRow; |
| | | import com.ruoyi.system.service.ITestLiveService.TestPlanSnapshot; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | |
| | | /** 将设备最新快照转换为测试视角,并托管 SSE 连接生命周期。 */ |
| | | @Service |
| | |
| | | private final ITTestService tests; |
| | | private final DeviceDiscoveryCandidateStore candidates; |
| | | private final BtsDevelopmentSimulator simulator; |
| | | private final TTestMapper mapper; |
| | | private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); |
| | | |
| | | @Autowired |
| | | public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates, |
| | | BtsDevelopmentSimulator simulator, TTestMapper mapper) |
| | | { this.tests = tests; this.candidates = candidates; this.simulator = simulator; this.mapper = mapper; } |
| | | |
| | | public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates, |
| | | BtsDevelopmentSimulator simulator) |
| | | { this.tests = tests; this.candidates = candidates; this.simulator = simulator; } |
| | | { this(tests, candidates, simulator, null); } |
| | | |
| | | @Override public LiveState current(Long testId) |
| | | { |
| | |
| | | .map(source -> Map.<String, Object>of("sourceType", source.sourceType(), "sourceId", source.sourceId(), |
| | | "batteryNo", source.batteryNo(), "status", source.deviceStatus(), "message", "数据来源当前不可观测")) |
| | | .toList()); |
| | | if (mapper != null) { |
| | | mapper.selectWarningExceptions(testId, "OPEN").forEach(item -> exceptions.add(Map.of( |
| | | "exceptionId", item.getExceptionId(), "sourceType", item.getSourceType(), |
| | | "metricCode", item.getMetricCode(), "value", item.getAbnormalValue(), "unit", item.getUnit(), |
| | | "status", item.getExceptionStatus(), "message", "测试预警异常"))); |
| | | } |
| | | String testStatus = test.getTestStatus(); |
| | | TestPlanSnapshot testPlan = detectorTestPlan(test); |
| | | return new LiveState(testId, testStatus, test.getReceptionStatus(), Instant.now(), sources, exceptions, testPlan); |
| | |
| | | import java.util.List; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord; |
| | |
| | | private final TTestMapper mapper; |
| | | private final ITTestService tests; |
| | | private final BtsDevelopmentSimulator simulator; |
| | | private final TestWarningExceptionService warningExceptions; |
| | | private final Map<String, Instant> lastSnapshotAt = new java.util.concurrent.ConcurrentHashMap<>(); |
| | | private final Map<Long, Instant> lastRawCaptureAt = new java.util.concurrent.ConcurrentHashMap<>(); |
| | | |
| | | public TestDataReceptionServiceImpl(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator) |
| | | @Autowired |
| | | public TestDataReceptionServiceImpl(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator, |
| | | TestWarningExceptionService warningExceptions) |
| | | { |
| | | this.mapper = mapper; |
| | | this.tests = tests; |
| | | this.simulator = simulator; |
| | | this.warningExceptions = warningExceptions; |
| | | } |
| | | |
| | | /** 保留单元测试及旧调用方构造契约。 */ |
| | | public TestDataReceptionServiceImpl(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator) |
| | | { this(mapper, tests, simulator, new TestWarningExceptionService(mapper)); } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | { |
| | | // 模拟器保留通道历史;只接收本次测试开始后产生的设备时间记录,避免顺序测试串入旧事实。 |
| | | if (startedAt != null && record.deviceTime().isBefore(startedAt.toInstant())) continue; |
| | | save(test, binding, record); |
| | | if (save(test, binding, record)) { |
| | | String source = String.join(":", java.util.Objects.toString(binding.getAdapterCode(), ""), |
| | | java.util.Objects.toString(binding.getDevtype(), ""), java.util.Objects.toString(binding.getDevid(), ""), |
| | | java.util.Objects.toString(binding.getSubdevid(), ""), java.util.Objects.toString(binding.getChlid(), "")); |
| | | warningExceptions.evaluate(test, "CABINET", source, "VOLTAGE", BigDecimal.valueOf(record.voltageV()), "V", Date.from(record.deviceTime())); |
| | | warningExceptions.evaluate(test, "CABINET", source, "CURRENT", BigDecimal.valueOf(record.currentA()), "A", Date.from(record.deviceTime())); |
| | | warningExceptions.evaluate(test, "CABINET", source, "CAPACITY", BigDecimal.valueOf(record.capacityAh()), "Ah", Date.from(record.deviceTime())); |
| | | warningExceptions.evaluate(test, "CABINET", source, "ENERGY", BigDecimal.valueOf(record.energyWh()), "Wh", Date.from(record.deviceTime())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | java.util.Objects.equals(mapping.getChannelCode(), measurement.channelCode()) |
| | | && metricMatches(mapping.getMeasurementCode(), measurement.metricCode())); |
| | | if (!mapped) continue; |
| | | saveCollector(test, battery, measurement); |
| | | if (saveCollector(test, battery, measurement)) |
| | | warningExceptions.evaluate(test, "COLLECTOR", measurement.adapterCode() + ":" + battery.getCollectorId() + ":" + measurement.channelCode(), measurement.metricCode(), measurement.value(), measurement.unit(), Date.from(measurement.deviceTime())); |
| | | } |
| | | } |
| | | |
| | | private void saveCollector(TTest test, TTestBattery battery, CollectorRealtimeMeasurement measurement) |
| | | private boolean saveCollector(TTest test, TTestBattery battery, CollectorRealtimeMeasurement measurement) |
| | | { |
| | | byte[] hash = hash(measurement); |
| | | String old = mapper.selectCollectorSampleContentHash(battery.getCollectorId(), measurement.channelCode(), |
| | |
| | | "采集器实时接收内容冲突 collector=" + battery.getCollectorId() + ", channel=" |
| | | + measurement.channelCode() + ", metric=" + measurement.metricCode() |
| | | + ", seqid=" + measurement.sequenceId()); |
| | | return; |
| | | return false; |
| | | } |
| | | Date receivedAt = new Date(); |
| | | int inserted = mapper.insertCollectorSample(test.getTestId(), battery.getBinding().getBindingId(), battery.getCollectorId(), |
| | |
| | | + measurement.channelCode() + ", metric=" + measurement.metricCode() |
| | | + ", seqid=" + measurement.sequenceId()); |
| | | } |
| | | return inserted > 0; |
| | | } |
| | | |
| | | private void save(TTest test, TTestChannelBinding binding, |
| | | private boolean save(TTest test, TTestChannelBinding binding, |
| | | BtsHistoryRecord record) |
| | | { |
| | | // 模拟器历史 testid 按通道固定;接收事实必须再带测试作用域,避免顺序测试共用全局幂等键。 |
| | |
| | | if (!old.equalsIgnoreCase(HexFormat.of().formatHex(hash))) |
| | | mapper.insertSampleConflict(test.getTestId(), binding.getChannelId(), |
| | | "实时接收内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId()); |
| | | return; |
| | | return false; |
| | | } |
| | | int inserted = mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(), |
| | | btsTestId, record.sequenceId(), Date.from(record.deviceTime()), |
| | |
| | | mapper.insertSampleConflict(test.getTestId(), binding.getChannelId(), |
| | | "实时接收并发内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId()); |
| | | } |
| | | return inserted > 0; |
| | | } |
| | | |
| | | private byte[] hash(BtsHistoryRecord record) |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Date; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import com.ruoyi.system.domain.TTest; |
| | | import com.ruoyi.system.domain.TTestWarningException; |
| | | import com.ruoyi.system.domain.TTestWarningThreshold; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | |
| | | /** 对成功接收的测量值执行测试阈值判断;设备状态和缺失数据不经过此服务。 */ |
| | | @Service |
| | | public class TestWarningExceptionService { |
| | | private final TTestMapper mapper; |
| | | public TestWarningExceptionService(TTestMapper mapper) { this.mapper = mapper; } |
| | | /** 仅对成功接收的非空测量值判定阈值,并聚合更新开放异常及规则快照。 */ |
| | | public void evaluate(TTest test, String sourceType, String sourceIdentity, String metricCode, BigDecimal value, String unit, Date occurredAt) { |
| | | if (value == null || test == null || test.getThresholds() == null) return; |
| | | for (TTestWarningThreshold rule : test.getThresholds()) { |
| | | if (rule == null || rule.getMetricCode() == null || !matches(rule.getMetricCode(), metricCode) |
| | | || !outside(value, rule.getMinValue(), rule.getMaxValue())) continue; |
| | | mapper.upsertWarningException(test.getTestId(), rule.getThresholdId(), sourceType, sourceIdentity, metricCode, value, unit, |
| | | JSON.toJSONString(rule), rule.getSeverity(), occurredAt == null ? new Date() : occurredAt); |
| | | } |
| | | } |
| | | |
| | | /** 由测试负责人关闭开放异常,并在同一事务追加不可变的测试审计事实。 */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int close(Long exceptionId, Long actorId, String actorName, String handlingNote) { |
| | | TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId); |
| | | if (exception == null) return 0; |
| | | int rows = mapper.closeWarningException(exceptionId, actorId, actorName, new Date(), handlingNote); |
| | | if (rows == 1) { |
| | | mapper.insertTestAudit(exception.getTestId(), "WARNING_EXCEPTION_CLOSED", actorId, actorName, null, |
| | | JSON.toJSONString(java.util.Map.of("exceptionId", exceptionId, "handlingNote", handlingNote))); |
| | | } |
| | | return rows; |
| | | } |
| | | private boolean outside(BigDecimal value, BigDecimal min, BigDecimal max) { return (min != null && value.compareTo(min) < 0) || (max != null && value.compareTo(max) > 0); } |
| | | private boolean matches(String configured, String actual) { |
| | | if (actual == null) return false; |
| | | if (configured.equalsIgnoreCase(actual)) return true; |
| | | String c = configured.toUpperCase(), a = actual.toUpperCase(); |
| | | return (c.contains("VOLTAGE") && a.contains("VOLTAGE")) || (c.contains("CURRENT") && a.contains("CURRENT")) |
| | | || (c.contains("CAPACITY") && a.contains("CAPACITY")) || (c.contains("ENERGY") && a.contains("ENERGY")) |
| | | || (c.contains("TEMPERATURE") && a.contains("TEMPERATURE")) || (c.contains("POSITIVE_POTENTIAL") && a.contains("POSITIVE_POTENTIAL")) |
| | | || (c.contains("NEGATIVE_POTENTIAL") && a.contains("NEGATIVE_POTENTIAL")); |
| | | } |
| | | } |
| | |
| | | <result property="severity" column="severity" /> |
| | | <result property="delFlag" column="del_flag" /> |
| | | </resultMap> |
| | | <resultMap id="TTestWarningExceptionResultMap" type="com.ruoyi.system.domain.TTestWarningException"> |
| | | <id property="exceptionId" column="exception_id"/><result property="testId" column="test_id"/> |
| | | <result property="testName" column="test_name"/><result property="sourceType" column="source_type"/> |
| | | <result property="sourceIdentity" column="source_identity"/><result property="metricCode" column="metric_code"/> |
| | | <result property="abnormalValue" column="abnormal_value"/><result property="unit" column="unit"/> |
| | | <result property="ruleSnapshot" column="rule_snapshot"/><result property="severity" column="severity"/> |
| | | <result property="exceptionStatus" column="exception_status"/><result property="firstOccurredAtUtc" column="first_occurred_at_utc"/> |
| | | <result property="lastOccurredAtUtc" column="last_occurred_at_utc"/><result property="occurrenceCount" column="occurrence_count"/> |
| | | <result property="handledBy" column="handled_by"/><result property="handledByName" column="handled_by_name"/> |
| | | <result property="handledAtUtc" column="handled_at_utc"/><result property="handlingNote" column="handling_note"/> |
| | | </resultMap> |
| | | |
| | | <resultMap id="TTestResultResultMap" type="TTestResult"> |
| | | <id property="resultId" column="result_id" /> |
| | |
| | | select threshold_id, test_id, metric_code, min_value, max_value, unit, severity, del_flag |
| | | from t_test_warning_threshold where test_id = #{testId} and del_flag = 0 order by threshold_id |
| | | </select> |
| | | <insert id="upsertWarningException"> |
| | | insert into t_test_warning_exception |
| | | (test_id, threshold_id, source_type, source_identity, metric_code, abnormal_value, unit, rule_snapshot, severity, |
| | | exception_status, first_occurred_at_utc, last_occurred_at_utc, occurrence_count) |
| | | values (#{testId}, #{thresholdId}, #{sourceType}, #{sourceIdentity}, #{metricCode}, #{value}, #{unit}, |
| | | cast(#{ruleSnapshot} as json), #{severity}, 'OPEN', #{occurredAt}, #{occurredAt}, 1) |
| | | on duplicate key update abnormal_value = values(abnormal_value), unit = values(unit), |
| | | last_occurred_at_utc = values(last_occurred_at_utc), occurrence_count = occurrence_count + 1, |
| | | source_identity = values(source_identity), severity = values(severity), rule_snapshot = values(rule_snapshot) |
| | | </insert> |
| | | <select id="selectWarningExceptions" resultMap="TTestWarningExceptionResultMap"> |
| | | select e.*, t.test_name, u.nick_name handled_by_name |
| | | from t_test_warning_exception e join t_test t on t.test_id=e.test_id |
| | | left join sys_user u on u.user_id=e.handled_by |
| | | <where><if test="testId != null">e.test_id = #{testId}</if><if test="status != null and status != ''">and e.exception_status = #{status}</if></where> |
| | | order by e.last_occurred_at_utc desc, e.exception_id desc |
| | | </select> |
| | | <select id="selectWarningExceptionById" parameterType="long" resultMap="TTestWarningExceptionResultMap"> |
| | | select e.*, t.test_name, u.nick_name handled_by_name from t_test_warning_exception e join t_test t on t.test_id=e.test_id |
| | | left join sys_user u on u.user_id=e.handled_by where e.exception_id=#{exceptionId} |
| | | </select> |
| | | <update id="closeWarningException"> |
| | | update t_test_warning_exception set exception_status='CLOSED', handled_by=#{handledBy}, handled_at_utc=#{handledAt}, handling_note=#{handlingNote} |
| | | where exception_id=#{exceptionId} and exception_status='OPEN' |
| | | </update> |
| | | <select id="selectReceivingTestIds" resultType="long"> |
| | | select test_id from t_test where del_flag = 0 and reception_status = 'RECEIVING' order by test_id |
| | | </select> |