From b78295e8da96c3e5e76befeef9582ce5273dbb74 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 29 九月 2026 13:10:23 +0800
Subject: [PATCH] 按代码规范检查整改实体注释、Service Javadoc 与分层边界

---
 battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java |  116 +++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 99 insertions(+), 17 deletions(-)

diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java b/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
index 3a6d632..29d0a6c 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
@@ -2,43 +2,125 @@
 
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 import com.alibaba.fastjson2.JSON;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.exception.ServiceException;
 import com.battery.business.domain.TTest;
 import com.battery.business.domain.TTestWarningException;
 import com.battery.business.domain.TTestWarningThreshold;
 import com.battery.business.mapper.TTestMapper;
 
-/** 对成功接收的测量值执行测试阈值判断;设备状态和缺失数据不经过此服务。 */
+/** 分别记录测量超限与接收中的离线事实;缺失测量不参与阈值判断。 */
 @Service
 public class TestWarningExceptionService {
     private final TTestMapper mapper;
     public TestWarningExceptionService(TTestMapper mapper) { this.mapper = mapper; }
-    /** 仅对成功接收的非空测量值判定阈值,并聚合更新开放异常及规则快照。 */
+
+    /**
+     * 记录接收中测试的绑定来源离线事实。
+     *
+     * <p>前置条件:调用方持有该测试的事务锁。只有测试处于“检测中 + 接收中”才判定离线,
+     * 离线不伪造测量值;暂停、停止、未开始接收时不新增或累加。</p>
+     *
+     * @param test       接收中的测试
+     * @param occurredAt 离线事实发生时间
+     */
+    public void recordOfflineSources(TTest test, Date occurredAt) {
+        // STEP 1: 只有“检测中 + 接收中”才判定离线,其他状态不新增也不累加。
+        if (test == null || !"RUNNING".equals(test.getTestStatus()) || !"RECEIVING".equals(test.getReceptionStatus())) return;
+        // STEP 2: 按测试和来源身份聚合离线异常,不写入测量值或阈值。
+        for (var source : mapper.selectOfflineTestSources(test.getTestId())) {
+            mapper.upsertOfflineException(test.getTestId(), source.sourceType(), source.sourceIdentity(), occurredAt);
+        }
+    }
+
+    /**
+     * 对接收中的一次成功实时测量判定超限并按聚合键写入开放异常。
+     *
+     * <p>前置条件:调用方持有该测试的事务锁。判定只依据成功观测值,不受采样入库周期限制,
+     * 正常观测不会自动关闭已有异常。</p>
+     *
+     * @param test        接收中的测试,须已加载阈值规则
+     * @param sourceType  来源类型:CABINET 检测柜、COLLECTOR 采集器
+     * @param sourceIdentity 来源身份
+     * @param metricCode  指标编码
+     * @param value       本次成功观测值;为空时不判定
+     * @param unit        观测值单位
+     * @param occurredAt  观测时间,为空时取当前时间
+     */
     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;
+        // STEP 1: 按阈值规则筛出被本次成功观测超限的规则,缺失测量不参与判定。
+        for (TTestWarningThreshold rule : exceededRules(test, metricCode, value)) {
+            // STEP 2: 以测试、指标和规则为聚合键原子累加开放异常,并保存触发时的规则快照。
             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);
-        return rows;
+    static List<TTestWarningThreshold> exceededRules(TTest test, String metricCode, BigDecimal value) {
+        if (value == null || test == null || test.getThresholds() == null
+                || !"RUNNING".equals(test.getTestStatus()) || !"RECEIVING".equals(test.getReceptionStatus())) return List.of();
+        return test.getThresholds().stream().filter(rule -> rule != null
+                && matchesMetric(rule.getMetricCode(), metricCode)
+                && outside(value, rule.getMinValue(), rule.getMaxValue())).toList();
     }
-    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;
+
+    /**
+     * 按测试和异常状态查询已持久化的阈值与离线异常事实。
+     *
+     * @param testId 测试主键,为空表示不限测试
+     * @param status 异常状态:OPEN 开放、CLOSED 已关闭;为空表示不限状态
+     * @return 按最近触发时间倒序排列的异常事实
+     */
+    public List<TTestWarningException> list(Long testId, String status) {
+        return mapper.selectWarningExceptions(testId, status);
+    }
+
+    /**
+     * 测试负责人关闭开放异常:校验归属与处理结果后写入,返回关闭后的异常事实。
+     *
+     * <p>前置条件:操作人必须是该异常所属测试的测试负责人,且处理结果必填。
+     * 只有开放异常可以关闭;关闭只改变处理状态,原始异常事实与累计触发次数仍保留。</p>
+     *
+     * @param exceptionId  异常主键
+     * @param actorId      操作人用户 ID,同时作为处理人写入
+     * @param actorName    操作人显示名称,随异常事实保存
+     * @param handlingNote 处理结果,必填
+     * @return 关闭后的异常事实
+     * @throws ServiceException 处理结果为空、异常不存在、操作人不是测试负责人或异常已关闭
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public TTestWarningException closeByPrincipal(Long exceptionId, Long actorId, String actorName, String handlingNote) {
+        // STEP 1: 处理结果为必填,先拒绝空提交。
+        String note = handlingNote == null ? null : handlingNote.trim();
+        if (note == null || note.isEmpty()) throw new ServiceException("处理结果不能为空");
+        // STEP 2: 异常与所属测试必须存在,且操作人就是该测试的测试负责人。
+        TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId);
+        if (exception == null) throw new ServiceException("异常不存在");
+        TTest test = mapper.selectTestById(exception.getTestId());
+        if (test == null || actorId == null || !actorId.equals(test.getPrincipalId()))
+            throw new ServiceException("仅测试负责人可以关闭预警异常");
+        // STEP 3: 仅开放异常可关闭,行数不为 1 说明已被并发关闭。
+        if (mapper.closeWarningException(exceptionId, actorId, actorName, new Date(), note) != 1)
+            throw new ServiceException("异常已关闭或不存在");
+        return mapper.selectWarningExceptionById(exceptionId);
+    }
+
+    private static boolean outside(BigDecimal value, BigDecimal min, BigDecimal max) { return (min != null && value.compareTo(min) < 0) || (max != null && value.compareTo(max) > 0); }
+    static boolean matchesCollectorMetric(String configured, String actual) {
+        if (configured == null || actual == null) return false;
+        String role = configured.toUpperCase(Locale.ROOT), metric = actual.toUpperCase(Locale.ROOT);
+        return role.equals(metric) || List.of("TEMPERATURE", "POSITIVE_POTENTIAL", "NEGATIVE_POTENTIAL").stream()
+                .anyMatch(code -> metric.equals(code) && role.contains(code));
+    }
+
+    static boolean matchesMetric(String configured, String actual) {
+        if (configured == null || actual == null) return false;
         if (configured.equalsIgnoreCase(actual)) return true;
-        String c = configured.toUpperCase(), a = actual.toUpperCase();
+        String c = configured.toUpperCase(Locale.ROOT), a = actual.toUpperCase(Locale.ROOT);
         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"))

--
Gitblit v1.7.1