Просмотр исходного кода

feat(api): 新增预警信息和设备状态管理功能

- 添加 EarlyWarning 实体类及映射器,用于预警信息的数据持久化
- 实现 EarlyWarningAutoService 接口及其实现类,支持采集数据自动生成预警
- 创建 EarlyWarningLevelUtil 工具类,用于计算预警等级和逼近程度
- 添加 EquipmentStatus 实体类及映射器,用于管理设备状态信息
- 实现 EquipmentStatusApiService 接口及控制器,提供设备报警状态更新功能
- 在各微服务中配置 RestTemplate 支持服务间调用
- 实现自动预警的创建、更新、关闭逻辑,支持多级别预警管理
林仔 1 месяц назад
Родитель
Сommit
11bd52e1c2

+ 16 - 0
firefighting-pressure-service/src/main/java/com/zksy/pressure/config/RestTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.zksy.pressure.config;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    @LoadBalanced
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 16 - 0
flammable-gas-service/src/main/java/com/zksy/gas/config/RestTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.zksy.gas.config;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    @LoadBalanced
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 16 - 0
radar-service/src/main/java/com/zksy/radar/config/RestTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.zksy.radar.config;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    @LoadBalanced
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 16 - 0
water-level-service/src/main/java/com/zksy/water/config/RestTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.zksy.water.config;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    @LoadBalanced
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 59 - 0
zk-api-service/src/main/java/com/zksy/api/controller/EquipmentStatusController.java

@@ -0,0 +1,59 @@
+package com.zksy.api.controller;
+
+import com.zksy.api.service.EquipmentStatusApiService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 设备状态统一更新接口
+ * 供各采集服务在告警处理后调用,同步设备报警状态
+ */
+@Slf4j
+@RestController
+@RequestMapping("/equipmentStatus")
+@Api(tags = "设备状态管理", description = "统一更新设备报警状态")
+public class EquipmentStatusController {
+
+    @Autowired
+    private EquipmentStatusApiService equipmentStatusApiService;
+
+    /**
+     * 更新设备报警状态
+     * @param params { deviceCode: "设备编码", alarmStatus: 0或1 }
+     * @return 更新结果
+     */
+    @PostMapping("/updateAlarmStatus")
+    @ApiOperation(value = "更新设备报警状态")
+    public Map<String, Object> updateAlarmStatus(@RequestBody Map<String, Object> params) {
+        String deviceCode = (String) params.get("deviceCode");
+        Integer alarmStatus = null;
+        Object alarmStatusObj = params.get("alarmStatus");
+        if (alarmStatusObj instanceof Integer) {
+            alarmStatus = (Integer) alarmStatusObj;
+        } else if (alarmStatusObj != null) {
+            alarmStatus = Integer.parseInt(alarmStatusObj.toString());
+        }
+
+        if (deviceCode == null || deviceCode.isEmpty()) {
+            log.warn("更新报警状态参数无效:deviceCode为空");
+            return Map.of("success", false, "msg", "deviceCode不能为空");
+        }
+        if (alarmStatus == null || (alarmStatus != 0 && alarmStatus != 1)) {
+            log.warn("更新报警状态参数无效:alarmStatus={}", alarmStatus);
+            return Map.of("success", false, "msg", "alarmStatus必须为0或1");
+        }
+
+        try {
+            boolean result = equipmentStatusApiService.updateAlarmStatus(deviceCode, alarmStatus);
+            return Map.of("success", result, "deviceCode", deviceCode, "alarmStatus", alarmStatus);
+        } catch (Exception e) {
+            log.error("更新设备报警状态异常: deviceCode={}, alarmStatus={}", deviceCode, alarmStatus, e);
+            return Map.of("success", false, "msg", "更新失败: " + e.getMessage());
+        }
+    }
+}

+ 111 - 0
zk-api-service/src/main/java/com/zksy/api/domain/EarlyWarning.java

@@ -0,0 +1,111 @@
+package com.zksy.api.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 预警信息(采集服务自动生成写入 early_warning)
+ */
+@TableName("early_warning")
+@Data
+public class EarlyWarning implements Serializable {
+
+    @TableId(value = "warning_id", type = IdType.ASSIGN_UUID)
+    @ApiModelProperty("预警ID")
+    private String warningId;
+
+    @TableField("warning_no")
+    @ApiModelProperty("预警编号")
+    private String warningNo;
+
+    @TableField("warning_name")
+    @ApiModelProperty("预警名称")
+    private String warningName;
+
+    @TableField("warning_type")
+    @ApiModelProperty("预警类型")
+    private String warningType;
+
+    @TableField("warning_level")
+    @ApiModelProperty("预警级别(1-4,1最高)")
+    private String warningLevel;
+
+    @TableField("warning_special")
+    @ApiModelProperty("预警专项")
+    private String warningSpecial;
+
+    @TableField("location")
+    @ApiModelProperty("预警位置")
+    private String location;
+
+    @TableField("longitude")
+    @ApiModelProperty("经度")
+    private BigDecimal longitude;
+
+    @TableField("latitude")
+    @ApiModelProperty("纬度")
+    private BigDecimal latitude;
+
+    @TableField("ownership_unit")
+    @ApiModelProperty("权属单位")
+    private String ownershipUnit;
+
+    @TableField("publisher")
+    @ApiModelProperty("发布人")
+    private String publisher;
+
+    @TableField("publish_time")
+    @ApiModelProperty("发布时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime publishTime;
+
+    @TableField("handler")
+    @ApiModelProperty("处置人")
+    private String handler;
+
+    @TableField("status")
+    @ApiModelProperty("状态")
+    private String status;
+
+    @TableField("warning_content")
+    @ApiModelProperty("预警内容描述")
+    private String warningContent;
+
+    @TableField("process_instance_id")
+    @ApiModelProperty("工作流实例ID")
+    private String processInstanceId;
+
+    @TableField("create_by")
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @TableField("create_time")
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField("update_by")
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
+    @TableField("update_time")
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableField("remark")
+    @ApiModelProperty("备注(自动预警标记:AUTO|deviceCode|warningCode)")
+    private String remark;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 60 - 0
zk-api-service/src/main/java/com/zksy/api/domain/EquipmentStatus.java

@@ -0,0 +1,60 @@
+package com.zksy.api.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 设备状态表(zk-api-service 轻量版)
+ */
+@Data
+@TableName(value = "equipment_status", schema = "public")
+public class EquipmentStatus {
+
+    @TableId(value = "status_id", type = IdType.AUTO)
+    private Integer statusId;
+
+    @TableField(value = "equipment_id")
+    private String equipmentId;
+
+    @TableField(value = "current_status")
+    private Integer currentStatus;
+
+    /** 报警状态(0-正常,1-报警) */
+    @TableField(value = "alarm_status")
+    private Integer alarmStatus;
+
+    @TableField(value = "online_status")
+    private Integer onlineStatus;
+
+    @TableField(value = "user_id")
+    private Long userId;
+
+    @TableField(value = "installation_addr")
+    private String installationAddr;
+
+    @TableField(value = "last_maintain_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date lastMaintainDate;
+
+    @TableField(value = "next_maintain_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date nextMaintainDate;
+
+    @TableField(value = "status_update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime statusUpdateTime;
+
+    @TableField(value = "create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField(value = "del_flag")
+    private String delFlag;
+}

+ 10 - 0
zk-api-service/src/main/java/com/zksy/api/mapper/EarlyWarningApiMapper.java

@@ -0,0 +1,10 @@
+package com.zksy.api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.api.domain.EarlyWarning;
+
+/**
+ * 预警信息 Mapper
+ */
+public interface EarlyWarningApiMapper extends BaseMapper<EarlyWarning> {
+}

+ 10 - 0
zk-api-service/src/main/java/com/zksy/api/mapper/EquipmentStatusApiMapper.java

@@ -0,0 +1,10 @@
+package com.zksy.api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.api.domain.EquipmentStatus;
+
+/**
+ * 设备状态Mapper(zk-api-service)
+ */
+public interface EquipmentStatusApiMapper extends BaseMapper<EquipmentStatus> {
+}

+ 35 - 0
zk-api-service/src/main/java/com/zksy/api/service/EarlyWarningAutoService.java

@@ -0,0 +1,35 @@
+package com.zksy.api.service;
+
+import java.math.BigDecimal;
+
+/**
+ * 采集数据自动生成预警
+ */
+public interface EarlyWarningAutoService {
+
+    /**
+     * 根据实际值与阈值评估并生成/更新/关闭自动预警。
+     * <ul>
+     *   <li>已越限:关闭未完结的自动预警(报警由调用方自行写入 alarm_data)</li>
+     *   <li>进入预警区:新增或更新 early_warning</li>
+     *   <li>回到安全区:关闭未完结的自动预警</li>
+     * </ul>
+     *
+     * @param deviceCode  设备编码
+     * @param warningType 预警类型名称
+     * @param warningCode 预警编码
+     * @param minValue    下限(可空)
+     * @param maxValue    上限(可空)
+     * @param actualValue 实际采集值
+     * @param extraRemark 附加说明(可空)
+     * @return 当前预警级别 1-4;未生成/已关闭时返回 null
+     */
+    Integer checkAndGenerate(String deviceCode, String warningType, String warningCode,
+                             BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue,
+                             String extraRemark);
+
+    /**
+     * 关闭指定设备+预警编码下未完结的自动预警
+     */
+    void closeAutoWarning(String deviceCode, String warningCode, String reason);
+}

+ 15 - 0
zk-api-service/src/main/java/com/zksy/api/service/EquipmentStatusApiService.java

@@ -0,0 +1,15 @@
+package com.zksy.api.service;
+
+/**
+ * 设备报警状态服务接口
+ */
+public interface EquipmentStatusApiService {
+
+    /**
+     * 更新设备报警状态
+     * @param deviceCode 设备编码
+     * @param alarmStatus 报警状态:0-正常,1-报警
+     * @return 是否更新成功
+     */
+    boolean updateAlarmStatus(String deviceCode, Integer alarmStatus);
+}

+ 223 - 0
zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

@@ -0,0 +1,223 @@
+package com.zksy.api.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.api.domain.EarlyWarning;
+import com.zksy.api.domain.EquipmentBase;
+import com.zksy.api.mapper.EarlyWarningApiMapper;
+import com.zksy.api.mapper.EquipmentBaseApiMapper;
+import com.zksy.api.service.EarlyWarningAutoService;
+import com.zksy.api.utils.EarlyWarningLevelUtil;
+import com.zksy.api.utils.EarlyWarningLevelUtil.EarlyWarningLevelResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 采集数据接近阈值时自动写入 early_warning
+ */
+@Slf4j
+@Service
+public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
+
+    private static final String AUTO_PREFIX = "AUTO|";
+    private static final String SYSTEM_USER = "系统自动";
+    private static final List<String> OPEN_STATUS = Arrays.asList(
+            "DRAFT", "PENDING", "PROCESSING", "RELEASED"
+    );
+
+    @Autowired
+    private EarlyWarningApiMapper earlyWarningApiMapper;
+
+    @Autowired
+    private EquipmentBaseApiMapper equipmentBaseApiMapper;
+
+    @Override
+    public Integer checkAndGenerate(String deviceCode, String warningType, String warningCode,
+                                    BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue,
+                                    String extraRemark) {
+        if (deviceCode == null || deviceCode.isEmpty() || warningCode == null || warningCode.isEmpty()) {
+            return null;
+        }
+        if (actualValue == null || (minValue == null && maxValue == null)) {
+            return null;
+        }
+
+        try {
+            // 已越限或回到安全区:关闭自动预警
+            if (EarlyWarningLevelUtil.isOverThreshold(actualValue, minValue, maxValue)) {
+                closeAutoWarning(deviceCode, warningCode, "已达报警阈值,转报警处理");
+                return null;
+            }
+
+            EarlyWarningLevelResult result = EarlyWarningLevelUtil.calculate(actualValue, minValue, maxValue);
+            if (result == null) {
+                closeAutoWarning(deviceCode, warningCode, "监测值已回落至安全区间");
+                return null;
+            }
+
+            EarlyWarning existing = findOpenAutoWarning(deviceCode, warningCode);
+            String content = buildContent(deviceCode, warningType, warningCode,
+                    minValue, maxValue, actualValue, result, extraRemark);
+
+            if (existing != null) {
+                updateExisting(existing, result, content, warningType);
+                return result.getLevel();
+            }
+
+            createNew(deviceCode, warningType, warningCode, result, content, minValue, maxValue, actualValue);
+            return result.getLevel();
+        } catch (Exception e) {
+            log.error("自动生成预警失败: deviceCode={}, warningCode={}", deviceCode, warningCode, e);
+            return null;
+        }
+    }
+
+    @Override
+    public void closeAutoWarning(String deviceCode, String warningCode, String reason) {
+        try {
+            EarlyWarning existing = findOpenAutoWarning(deviceCode, warningCode);
+            if (existing == null) {
+                return;
+            }
+            existing.setStatus("CLOSED");
+            existing.setUpdateBy(SYSTEM_USER);
+            existing.setUpdateTime(LocalDateTime.now());
+            if (reason != null && !reason.isEmpty()) {
+                String content = existing.getWarningContent() == null ? "" : existing.getWarningContent();
+                existing.setWarningContent(content + "【系统自动解除】" + reason);
+            }
+            earlyWarningApiMapper.updateById(existing);
+            log.info("自动关闭预警: warningNo={}, deviceCode={}, warningCode={}, reason={}",
+                    existing.getWarningNo(), deviceCode, warningCode, reason);
+        } catch (Exception e) {
+            log.error("关闭自动预警失败: deviceCode={}, warningCode={}", deviceCode, warningCode, e);
+        }
+    }
+
+    private EarlyWarning findOpenAutoWarning(String deviceCode, String warningCode) {
+        String marker = buildRemarkMarker(deviceCode, warningCode);
+        return earlyWarningApiMapper.selectOne(new LambdaQueryWrapper<EarlyWarning>()
+                .eq(EarlyWarning::getRemark, marker)
+                .in(EarlyWarning::getStatus, OPEN_STATUS)
+                .orderByDesc(EarlyWarning::getCreateTime)
+                .last("LIMIT 1"));
+    }
+
+    private void updateExisting(EarlyWarning existing, EarlyWarningLevelResult result,
+                                String content, String warningType) {
+        String newLevel = String.valueOf(result.getLevel());
+        boolean levelChanged = !newLevel.equals(existing.getWarningLevel());
+        existing.setWarningLevel(newLevel);
+        existing.setWarningContent(content);
+        if (warningType != null && !warningType.isEmpty()) {
+            existing.setWarningType(warningType);
+            existing.setWarningName(buildWarningName(warningType, result.getLevel()));
+        }
+        existing.setUpdateBy(SYSTEM_USER);
+        existing.setUpdateTime(LocalDateTime.now());
+        earlyWarningApiMapper.updateById(existing);
+        if (levelChanged) {
+            log.info("自动更新预警级别: warningNo={}, level={}级, approachRatio={}%",
+                    existing.getWarningNo(), result.getLevel(), result.getApproachRatio());
+        }
+    }
+
+    private void createNew(String deviceCode, String warningType, String warningCode,
+                           EarlyWarningLevelResult result, String content,
+                           BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue) {
+        EquipmentBase equipment = findEquipment(deviceCode);
+        LocalDateTime now = LocalDateTime.now();
+
+        EarlyWarning warning = new EarlyWarning();
+        warning.setWarningNo(generateWarningNo());
+        warning.setWarningName(buildWarningName(
+                warningType != null ? warningType : warningCode, result.getLevel()));
+        warning.setWarningType(warningType != null && !warningType.isEmpty() ? warningType : warningCode);
+        warning.setWarningLevel(String.valueOf(result.getLevel()));
+        warning.setWarningSpecial(warningCode);
+        warning.setStatus("RELEASED");
+        warning.setPublisher(SYSTEM_USER);
+        warning.setPublishTime(now);
+        warning.setCreateBy(SYSTEM_USER);
+        warning.setCreateTime(now);
+        warning.setUpdateTime(now);
+        warning.setWarningContent(content);
+        warning.setRemark(buildRemarkMarker(deviceCode, warningCode));
+
+        if (equipment != null) {
+            warning.setLocation(equipment.getEquipmentLocation());
+            warning.setLongitude(equipment.getLongitude());
+            warning.setLatitude(equipment.getLatitude());
+            warning.setOwnershipUnit(equipment.getOwnershipUnit());
+            warning.setHandler(equipment.getMaintainer());
+            if (equipment.getEquipmentName() != null) {
+                warning.setWarningName(equipment.getEquipmentName() + "-"
+                        + buildWarningName(warningType != null ? warningType : warningCode, result.getLevel()));
+            }
+        } else {
+            warning.setLocation("设备编码:" + deviceCode);
+        }
+
+        earlyWarningApiMapper.insert(warning);
+        log.info("自动生成预警成功: warningNo={}, deviceCode={}, warningCode={}, level={}级, actual={}, min={}, max={}",
+                warning.getWarningNo(), deviceCode, warningCode, result.getLevel(),
+                actualValue, minValue, maxValue);
+    }
+
+    private EquipmentBase findEquipment(String deviceCode) {
+        try {
+            return equipmentBaseApiMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()
+                    .eq(EquipmentBase::getEquipmentCode, deviceCode)
+                    .last("LIMIT 1"));
+        } catch (Exception e) {
+            log.warn("查询设备信息失败: deviceCode={}", deviceCode);
+            return null;
+        }
+    }
+
+    private static String buildRemarkMarker(String deviceCode, String warningCode) {
+        return AUTO_PREFIX + deviceCode + "|" + warningCode;
+    }
+
+    private static String buildWarningName(String warningType, int level) {
+        return warningType + "-接近阈值预警(" + EarlyWarningLevelUtil.getLevelName(level) + ")";
+    }
+
+    private static String buildContent(String deviceCode, String warningType, String warningCode,
+                                       BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue,
+                                       EarlyWarningLevelResult result, String extraRemark) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("【系统自动预警】设备[").append(deviceCode).append("]");
+        if (warningType != null) {
+            sb.append(" ").append(warningType);
+        }
+        sb.append("(").append(warningCode).append(")");
+        sb.append(" 当前值=").append(actualValue);
+        sb.append(",阈值范围=[")
+                .append(minValue != null ? minValue : "-")
+                .append(", ")
+                .append(maxValue != null ? maxValue : "-")
+                .append("]");
+        sb.append(",逼近侧=").append("UPPER".equals(result.getSide()) ? "上限" : "下限");
+        sb.append(",逼近率=").append(result.getApproachRatio()).append("%");
+        sb.append(",级别=").append(EarlyWarningLevelUtil.getLevelName(result.getLevel()));
+        sb.append("。数据已接近阈值但尚未越限,请关注。");
+        if (extraRemark != null && !extraRemark.isEmpty()) {
+            sb.append(" 备注:").append(extraRemark);
+        }
+        return sb.toString();
+    }
+
+    private static String generateWarningNo() {
+        String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        String random = String.format("%04d", new Random().nextInt(10000));
+        return "YJ" + ts + random;
+    }
+}

+ 78 - 0
zk-api-service/src/main/java/com/zksy/api/service/impl/EquipmentStatusApiServiceImpl.java

@@ -0,0 +1,78 @@
+package com.zksy.api.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.api.domain.EquipmentBase;
+import com.zksy.api.domain.EquipmentStatus;
+import com.zksy.api.mapper.EquipmentBaseApiMapper;
+import com.zksy.api.mapper.EquipmentStatusApiMapper;
+import com.zksy.api.service.EquipmentStatusApiService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备报警状态服务实现
+ * 根据设备编码更新 equipment_status 表的报警状态
+ */
+@Slf4j
+@Service
+public class EquipmentStatusApiServiceImpl implements EquipmentStatusApiService {
+
+    @Autowired
+    private EquipmentBaseApiMapper equipmentBaseApiMapper;
+
+    @Autowired
+    private EquipmentStatusApiMapper equipmentStatusApiMapper;
+
+    @Override
+    @Transactional
+    public boolean updateAlarmStatus(String deviceCode, Integer alarmStatus) {
+        if (deviceCode == null || deviceCode.isEmpty()) {
+            log.warn("更新设备报警状态失败:deviceCode为空");
+            return false;
+        }
+
+        // 1. 根据设备编码查询设备基础信息,获取 equipmentId
+        EquipmentBase equipmentBase = equipmentBaseApiMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .eq(EquipmentBase::getEquipmentCode, deviceCode)
+                        .last("LIMIT 1"));
+        if (equipmentBase == null) {
+            log.warn("更新设备报警状态失败:未找到设备编码对应的设备信息,deviceCode={}", deviceCode);
+            return false;
+        }
+
+        String equipmentId = equipmentBase.getEquipmentId();
+
+        // 2. 查询是否已有状态记录
+        EquipmentStatus existingStatus = equipmentStatusApiMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentStatus>()
+                        .eq(EquipmentStatus::getEquipmentId, equipmentId));
+
+        if (existingStatus != null) {
+            // 更新已有记录
+            existingStatus.setAlarmStatus(alarmStatus);
+            existingStatus.setStatusUpdateTime(LocalDateTime.now());
+            equipmentStatusApiMapper.updateById(existingStatus);
+            log.info("更新设备报警状态成功: deviceCode={}, equipmentId={}, alarmStatus={}",
+                    deviceCode, equipmentId, alarmStatus);
+        } else {
+            // 不存在则创建新记录
+            EquipmentStatus newStatus = new EquipmentStatus();
+            newStatus.setEquipmentId(equipmentId);
+            newStatus.setAlarmStatus(alarmStatus);
+            newStatus.setCurrentStatus(1); // 默认在用
+            newStatus.setOnlineStatus(1);  // 默认在线
+            newStatus.setStatusUpdateTime(LocalDateTime.now());
+            newStatus.setCreateTime(LocalDateTime.now());
+            equipmentStatusApiMapper.insert(newStatus);
+            log.info("创建设备报警状态记录成功: deviceCode={}, equipmentId={}, alarmStatus={}",
+                    deviceCode, equipmentId, alarmStatus);
+        }
+
+        return true;
+    }
+}

+ 204 - 0
zk-api-service/src/main/java/com/zksy/api/utils/EarlyWarningLevelUtil.java

@@ -0,0 +1,204 @@
+package com.zksy.api.utils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 预警等级计算工具(数据接近阈值但尚未越限时使用)
+ * <p>
+ * 预警区:距上下限剩余空间进入正常区间宽度(或单侧阈值绝对值)的 30% 时进入预警。
+ * 级别 1 最高,4 最低,按逼近程度定级。
+ */
+public class EarlyWarningLevelUtil {
+
+    /** 预警带宽占正常区间(或单侧阈值绝对值)的比例 */
+    private static final BigDecimal BAND_RATIO = new BigDecimal("0.30");
+
+    private static final BigDecimal FORTY = new BigDecimal("40");
+    private static final BigDecimal SIXTY_FIVE = new BigDecimal("65");
+    private static final BigDecimal EIGHTY_FIVE = new BigDecimal("85");
+    private static final BigDecimal HUNDRED = new BigDecimal("100");
+
+    private EarlyWarningLevelUtil() {
+    }
+
+    /**
+     * 计算预警级别结果;未进入预警区或已越限时返回 null(应由报警逻辑处理)
+     *
+     * @param actual 实际值
+     * @param min    下限(可空)
+     * @param max    上限(可空)
+     * @return 预警级别结果,无需预警时为 null
+     */
+    public static EarlyWarningLevelResult calculate(BigDecimal actual, BigDecimal min, BigDecimal max) {
+        if (actual == null || (min == null && max == null)) {
+            return null;
+        }
+
+        // 已越限:不生成预警
+        if (isOverThreshold(actual, min, max)) {
+            return null;
+        }
+
+        BigDecimal band = calcBand(min, max);
+        if (band == null || band.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+
+        EarlyWarningLevelResult upper = evaluateUpper(actual, max, band);
+        EarlyWarningLevelResult lower = evaluateLower(actual, min, band);
+
+        if (upper == null) {
+            return lower;
+        }
+        if (lower == null) {
+            return upper;
+        }
+        // 双侧同时接近时,取更紧急的级别(数值更小)
+        return upper.getLevel() <= lower.getLevel() ? upper : lower;
+    }
+
+    /**
+     * 是否已达到或超过报警阈值
+     */
+    public static boolean isOverThreshold(BigDecimal actual, BigDecimal min, BigDecimal max) {
+        if (actual == null) {
+            return false;
+        }
+        if (min != null && actual.compareTo(min) <= 0) {
+            return true;
+        }
+        return max != null && actual.compareTo(max) >= 0;
+    }
+
+    /**
+     * 是否处于预警区(接近但未越限)
+     */
+    public static boolean isInWarningZone(BigDecimal actual, BigDecimal min, BigDecimal max) {
+        return calculate(actual, min, max) != null;
+    }
+
+    private static EarlyWarningLevelResult evaluateUpper(BigDecimal actual, BigDecimal max, BigDecimal band) {
+        if (max == null) {
+            return null;
+        }
+        BigDecimal warnStart = max.subtract(band);
+        // 进入上限预警区:warnStart <= actual < max
+        if (actual.compareTo(warnStart) < 0 || actual.compareTo(max) >= 0) {
+            return null;
+        }
+        BigDecimal approachRatio = actual.subtract(warnStart)
+                .divide(band, 4, RoundingMode.HALF_UP)
+                .multiply(HUNDRED);
+        return new EarlyWarningLevelResult(mapToLevel(approachRatio), approachRatio, "UPPER");
+    }
+
+    private static EarlyWarningLevelResult evaluateLower(BigDecimal actual, BigDecimal min, BigDecimal band) {
+        if (min == null) {
+            return null;
+        }
+        BigDecimal warnEnd = min.add(band);
+        // 进入下限预警区:min < actual <= warnEnd
+        if (actual.compareTo(min) <= 0 || actual.compareTo(warnEnd) > 0) {
+            return null;
+        }
+        // 逼近率:刚进入 warnEnd 为 0%,贴近 min 为 100%
+        BigDecimal approachRatio = warnEnd.subtract(actual)
+                .divide(band, 4, RoundingMode.HALF_UP)
+                .multiply(HUNDRED);
+        if (approachRatio.compareTo(BigDecimal.ZERO) < 0) {
+            approachRatio = BigDecimal.ZERO;
+        }
+        if (approachRatio.compareTo(HUNDRED) > 0) {
+            approachRatio = HUNDRED;
+        }
+        return new EarlyWarningLevelResult(mapToLevel(approachRatio), approachRatio, "LOWER");
+    }
+
+    private static BigDecimal calcBand(BigDecimal min, BigDecimal max) {
+        if (min != null && max != null) {
+            BigDecimal range = max.subtract(min).abs();
+            if (range.compareTo(BigDecimal.ZERO) == 0) {
+                BigDecimal base = max.abs().compareTo(BigDecimal.ZERO) > 0 ? max.abs() : BigDecimal.ONE;
+                return base.multiply(BAND_RATIO);
+            }
+            return range.multiply(BAND_RATIO);
+        }
+        if (max != null) {
+            BigDecimal abs = max.abs();
+            return abs.compareTo(BigDecimal.ZERO) > 0 ? abs.multiply(BAND_RATIO) : BigDecimal.ONE;
+        }
+        if (min != null) {
+            BigDecimal abs = min.abs();
+            return abs.compareTo(BigDecimal.ZERO) > 0 ? abs.multiply(BAND_RATIO) : BigDecimal.ONE;
+        }
+        return null;
+    }
+
+    /**
+     * 逼近率 → 预警级别
+     * <ul>
+     *   <li>[0, 40) → 4 提醒(刚进入预警区)</li>
+     *   <li>[40, 65) → 3 警告</li>
+     *   <li>[65, 85) → 2 高危</li>
+     *   <li>[85, 100] → 1 紧急(即将越限)</li>
+     * </ul>
+     */
+    public static int mapToLevel(BigDecimal approachRatio) {
+        if (approachRatio == null) {
+            return 4;
+        }
+        if (approachRatio.compareTo(FORTY) < 0) {
+            return 4;
+        }
+        if (approachRatio.compareTo(SIXTY_FIVE) < 0) {
+            return 3;
+        }
+        if (approachRatio.compareTo(EIGHTY_FIVE) < 0) {
+            return 2;
+        }
+        return 1;
+    }
+
+    public static String getLevelName(int level) {
+        switch (level) {
+            case 1:
+                return "1级(紧急)";
+            case 2:
+                return "2级(高危)";
+            case 3:
+                return "3级(警告)";
+            case 4:
+                return "4级(提醒)";
+            default:
+                return "未知等级";
+        }
+    }
+
+    /**
+     * 预警级别计算结果
+     */
+    public static class EarlyWarningLevelResult {
+        private final int level;
+        private final BigDecimal approachRatio;
+        private final String side;
+
+        public EarlyWarningLevelResult(int level, BigDecimal approachRatio, String side) {
+            this.level = level;
+            this.approachRatio = approachRatio;
+            this.side = side;
+        }
+
+        public int getLevel() {
+            return level;
+        }
+
+        public BigDecimal getApproachRatio() {
+            return approachRatio;
+        }
+
+        public String getSide() {
+            return side;
+        }
+    }
+}