From 76cd5948e95ddbde9dd4411b466ee75ba20b562e Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 03 九月 2026 17:28:29 +0800
Subject: [PATCH] 实现测试预警异常管理
---
ruoyi-admin/src/main/resources/db/migration/V41__test_warning_exceptions.sql | 23 +++
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestWarningExceptionServiceTest.java | 60 ++++++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestWarningExceptionService.java | 51 ++++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java | 16 ++
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml | 36 ++++++
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestWarningException.java | 46 +++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java | 15 ++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestWarningExceptionController.java | 47 +++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java | 34 ++++-
9 files changed, 320 insertions(+), 8 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestWarningExceptionController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestWarningExceptionController.java
new file mode 100644
index 0000000..3d96d85
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestWarningExceptionController.java
@@ -0,0 +1,47 @@
+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) {}
+}
diff --git a/ruoyi-admin/src/main/resources/db/migration/V41__test_warning_exceptions.sql b/ruoyi-admin/src/main/resources/db/migration/V41__test_warning_exceptions.sql
new file mode 100644
index 0000000..b4dddf6
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V41__test_warning_exceptions.sql
@@ -0,0 +1,23 @@
+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='测试预警阈值异常';
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestWarningExceptionServiceTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestWarningExceptionServiceTest.java
new file mode 100644
index 0000000..536c497
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestWarningExceptionServiceTest.java
@@ -0,0 +1,60 @@
+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;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestWarningException.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestWarningException.java
new file mode 100644
index 0000000..cce2cdb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestWarningException.java
@@ -0,0 +1,46 @@
+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;}
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
index fce88a0..e5a2f5f 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -11,6 +11,7 @@
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;
@@ -51,6 +52,20 @@
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,
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
index 2b24d8e..1869330 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
@@ -13,6 +13,7 @@
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;
@@ -26,6 +27,7 @@
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
@@ -34,11 +36,17 @@
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)
{
@@ -60,6 +68,12 @@
.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);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
index a0f426c..107d501 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
@@ -11,6 +11,7 @@
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;
@@ -30,15 +31,23 @@
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)
@@ -169,7 +178,15 @@
{
// 模拟器保留通道历史;只接收本次测试开始后产生的设备时间记录,避免顺序测试串入旧事实。
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()));
+ }
}
}
}
@@ -192,11 +209,12 @@
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(),
@@ -209,7 +227,7 @@
"采集器实时接收内容冲突 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(),
@@ -228,9 +246,10 @@
+ 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 按通道固定;接收事实必须再带测试作用域,避免顺序测试共用全局幂等键。
@@ -242,7 +261,7 @@
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()),
@@ -257,6 +276,7 @@
mapper.insertSampleConflict(test.getTestId(), binding.getChannelId(),
"实时接收并发内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId());
}
+ return inserted > 0;
}
private byte[] hash(BtsHistoryRecord record)
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestWarningExceptionService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestWarningExceptionService.java
new file mode 100644
index 0000000..df120bf
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestWarningExceptionService.java
@@ -0,0 +1,51 @@
+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"));
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index 3c7a660..ad993ce 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -100,6 +100,17 @@
<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" />
@@ -317,6 +328,31 @@
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>
--
Gitblit v1.7.1