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

实现阿里云发送短信服务

zlm 9 месяцев назад
Родитель
Сommit
35c3801a33

+ 10 - 0
environment-service/pom.xml

@@ -23,6 +23,11 @@
             <artifactId>zk-common</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-api-service</artifactId>
+            <version>1.0.0</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
@@ -77,6 +82,11 @@
             <artifactId>net-device</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.54</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 15 - 0
environment-service/src/main/java/com/zksy/environment/config/RSServerService.java

@@ -1,5 +1,6 @@
 package com.zksy.environment.config;
 
+import com.zksy.api.utils.SmsUtil;
 import com.zksy.environment.domain.ERealTimeData;
 import com.zksy.environment.mapper.ERealTimeDataMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -11,6 +12,8 @@ import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
@@ -18,6 +21,7 @@ import java.util.concurrent.TimeUnit;
 
 @Component
 @Slf4j
+
 public class RSServerService {
 
     private RSServer rsServer;
@@ -32,6 +36,9 @@ public class RSServerService {
     @Autowired
     private ERealTimeDataMapper realTimeDataMapper;
 
+    @Autowired
+    private SmsUtil smsUtil;
+
     @PostConstruct
     public void init() {
         log.info("RSServer服务开始初始化,监听端口:9801");
@@ -100,6 +107,12 @@ public class RSServerService {
 
                 @Override
                 public void receiveRealtimeData(RealTimeData data) {
+                    // 1. 定义必须传入的报警手机号列表(可根据实际场景从配置/数据库获取)
+                    List<String> mandatoryAlarmPhones = Arrays.asList(
+                            "15773238205",
+                            "19892389826"
+                            // 可根据设备类型/节点ID动态调整
+                    );
                     for (NodeData nd : data.getNodeList()) {
                         try {
                             ERealTimeData realTimeData = new ERealTimeData();
@@ -115,6 +128,8 @@ public class RSServerService {
                             realTimeData.setCreateTime(LocalDateTime.now());
                             realTimeDataMapper.insert(realTimeData);
 
+                            // 2. 必须传入第三个参数(报警手机号列表)
+                            smsUtil.checkDeviceAlarmAndSend(data, nd, mandatoryAlarmPhones);
                         } catch (Exception e) {
                             log.error("实时数据入库失败:设备ID={}, 节点ID={}", data.getDeviceId(), nd.getNodeId(), e);
                         }

+ 1 - 1
pom.xml

@@ -35,7 +35,7 @@
         <okhttp.version>4.9.3</okhttp.version>
         <redis.version>3.0.5</redis.version>
         <sentinel.version>2021.0.4.0</sentinel.version>
-        <minioutil.version>1.0.0</minioutil.version>
+        <minioutil.version>1.0.1</minioutil.version>
         <mqtt.version>5.5.9</mqtt.version>
         <netty.version>4.1.75.Final</netty.version>
         <redisson.version>3.13.6</redisson.version>

+ 17 - 1
zk-api-service/pom.xml

@@ -7,7 +7,7 @@
         <artifactId>pipe-ner-server</artifactId>
         <version>1.0.0</version>
     </parent>
-    <groupId>org.example</groupId>
+<!--    <groupId>org.example</groupId>-->
     <artifactId>zk-api-service</artifactId>
 
     <properties>
@@ -72,6 +72,22 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>
+        <!--    阿里云发送短信服务-->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibabacloud-dysmsapi20170525</artifactId>
+            <version>4.0.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>net-device</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.54</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 21 - 0
zk-api-service/src/main/java/com/zksy/api/config/AliyunSmsConfig.java

@@ -0,0 +1,21 @@
+package com.zksy.api.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Component
+@ConfigurationProperties(prefix = "aliyun.sms")
+public class AliyunSmsConfig {
+    private String accessKeyId;
+    private String accessKeySecret;
+    private String signName;
+    private String templateCode;
+    private String regionId;
+
+}

+ 16 - 0
zk-api-service/src/main/java/com/zksy/api/domain/Enum/DeviceCodeEnum.java

@@ -0,0 +1,16 @@
+package com.zksy.api.domain.Enum;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备编码枚举(新增)
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceCodeEnum {
+    ENVIRONMENT_DEVICE("21119740", "环境设备"); // 对应数据库中的设备编码
+
+    private final String code;  // 设备编码
+    private final String name;  // 设备名称
+}

+ 21 - 0
zk-api-service/src/main/java/com/zksy/api/domain/Enum/WarningCodeEnum.java

@@ -0,0 +1,21 @@
+package com.zksy.api.domain.Enum;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 预警编码枚举(替代原WarningTypeEnum)
+ */
+@Getter
+@AllArgsConstructor
+public enum WarningCodeEnum {
+    SUSPENDED_SOLIDS("WARN-SUSPENDED-SOLIDS", "悬浮物预警", 40.0),
+    COD("WARN-COD", "COD预警", 30.0),
+    AMMONIA_NITROGEN("WARN-AMMONIA-NITROGEN", "氨氮预警", 20.0),
+    CONDUCTIVITY("WARN-CONDUCTIVITY", "电导率预警", 20.0),
+    PH("WARN-PH", "PH预警", 20.0);
+
+    private final String code;       // 预警编码(对应数据库)
+    private final String name;       // 预警名称
+    private final double defaultVal; // 默认阈值(数据库未配置时使用)
+}

+ 5 - 0
zk-api-service/src/main/java/com/zksy/api/service/WarningThresholdService.java

@@ -3,6 +3,7 @@ package com.zksy.api.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.api.domain.WarningThreshold;
+import org.springframework.stereotype.Service;
 
 import java.util.List;
 
@@ -15,4 +16,8 @@ public interface WarningThresholdService extends IService<WarningThreshold> {
     Page<WarningThreshold> findByPage(long pageNum, long pageSize, String deviceName,String deviceCode,String warningType,String warningCode);
     List<WarningThreshold> getWarningThresholdList(String deviceName,String deviceCode,String warningType,String warningCode);
 
+    /**
+     * 根据设备编号查询该设备所有预警值
+     */
+    WarningThreshold getWarningThresholdByDeviceAndCode(String deviceCode, String warningCode);
 }

+ 13 - 0
zk-api-service/src/main/java/com/zksy/api/service/impl/WarningThresholdServiceImpl.java

@@ -1,6 +1,7 @@
 package com.zksy.api.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.api.domain.WarningThreshold;
@@ -41,6 +42,18 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
         queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
         return this.list(queryWrapper);
     }
+
+    /**
+     * 根据设备编号查询该设备所有预警值
+     */
+    @Override
+    public WarningThreshold getWarningThresholdByDeviceAndCode(String deviceCode, String warningCodes) {
+        LambdaQueryWrapper<WarningThreshold> lambdaQueryWrapper = Wrappers.lambdaQuery(WarningThreshold.class)
+                .eq(WarningThreshold::getDeviceCode, deviceCode)
+                .eq(WarningThreshold::getWarningCode,warningCodes);
+
+        return baseMapper.selectOne(lambdaQueryWrapper);
+    }
 }
 
 

+ 294 - 0
zk-api-service/src/main/java/com/zksy/api/utils/SmsUtil.java

@@ -0,0 +1,294 @@
+package com.zksy.api.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.auth.credentials.Credential;
+import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
+import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient;
+import com.aliyun.sdk.service.dysmsapi20170525.models.*;
+import com.google.gson.Gson;
+import com.zksy.api.config.AliyunSmsConfig;
+import com.zksy.api.domain.Enum.DeviceCodeEnum;
+import com.zksy.api.domain.Enum.WarningCodeEnum;
+import com.zksy.api.domain.WarningThreshold;
+import com.zksy.api.service.WarningThresholdService;
+import darabonba.core.client.ClientOverrideConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import rk.netDevice.sdk.p2.NodeData;
+import rk.netDevice.sdk.p2.RealTimeData;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class SmsUtil {
+
+    private final AliyunSmsConfig aliyunSmsConfig;
+    private final AsyncClient client;
+
+    private final WarningThresholdService service;
+
+    // 报警缓存和冷却时间
+//    private static final Map<String, Long> ALARM_CACHE = new ConcurrentHashMap<>();
+//    private static final long ALARM_COOLDOWN_MS = 5 * 60 * 1000;
+    //TODO 假数据
+    private static final List<String> DEFAULT_ALARM_PHONES = Arrays.asList(
+            "15773238205", "19892389826"
+    );
+
+    @Autowired
+    public SmsUtil(AliyunSmsConfig aliyunSmsConfig,WarningThresholdService service) {
+        this.aliyunSmsConfig = aliyunSmsConfig;
+        this.service=service;
+
+        try {
+            StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
+                    .accessKeyId(aliyunSmsConfig.getAccessKeyId())
+                    .accessKeySecret(aliyunSmsConfig.getAccessKeySecret())
+                    .build());
+
+            this.client = AsyncClient.builder()
+                    .region(aliyunSmsConfig.getRegionId())
+                    .credentialsProvider(provider)
+                    .overrideConfiguration(
+                            ClientOverrideConfiguration.create()
+                                    .setEndpointOverride("dysmsapi.aliyuncs.com")
+                                    .setConnectTimeout(Duration.ofSeconds(30))
+                    )
+                    .build();
+        } catch (Exception e) {
+            log.error("阿里云短信客户端初始化失败", e);
+            throw new RuntimeException("短信服务初始化异常", e); // 初始化失败应快速失败
+        }
+    }
+
+    /**
+     * 发送单条短信
+     * @param phoneNumber 接收短信的手机号
+     * @param templateParam 短信模板参数,JSON格式字符串
+     * @return 是否发送成功
+     */
+    public boolean sendSms(String phoneNumber, String templateParam) {
+        try {
+            // 构建发送短信请求
+            SendSmsRequest sendSmsRequest = SendSmsRequest.builder()
+                    .phoneNumbers(phoneNumber)
+                    .signName(aliyunSmsConfig.getSignName())
+                    .templateCode(aliyunSmsConfig.getTemplateCode())
+                    .templateParam(templateParam)
+                    .build();
+
+            // 发送短信
+            CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
+            SendSmsResponse resp = response.get();
+
+            // 处理响应结果
+            SendSmsResponseBody body = resp.getBody();
+            System.out.println("短信发送响应: " + new Gson().toJson(body));
+
+            // 返回发送结果,"OK"表示成功
+            return "OK".equals(body.getCode());
+        } catch (Exception e) {
+            System.err.println("发送短信失败: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+    /**
+     * 批量发送相同内容的短信给多个手机号
+     * @param phoneNumbers 接收短信的手机号列表
+     * @param templateParam 短信模板参数,JSON格式字符串
+     * @return 发送结果,key为手机号,value为是否成功
+     */
+    public Map<String, Boolean> sendBatchSms(List<String> phoneNumbers, String templateParam) {
+        // 入参校验
+        if (phoneNumbers == null || phoneNumbers.isEmpty()) {
+            log.warn("批量发送短信失败:手机号列表为空");
+            return Collections.emptyMap();
+        }
+
+        try {
+            //生成与手机号数量相同的签名列表
+            List<String> signNames = phoneNumbers.stream()
+                    .map(phone -> aliyunSmsConfig.getSignName()) // 复制签名,与手机号数量一致
+                    .collect(Collectors.toList());
+
+            //生成与手机号数量相同的模板参数列表
+            List<String> templateParams = phoneNumbers.stream()
+                    .map(phone -> templateParam) // 复制参数,与手机号数量一致
+                    .collect(Collectors.toList());
+
+            //构建批量发送请求
+            SendBatchSmsRequest request = SendBatchSmsRequest.builder()
+                    .phoneNumberJson(new Gson().toJson(phoneNumbers)) // 手机号列表
+                    .signNameJson(new Gson().toJson(signNames)) // 签名列表
+                    .templateCode(aliyunSmsConfig.getTemplateCode())
+                    .templateParamJson(new Gson().toJson(templateParams)) // 参数列表
+                    .build();
+
+            // 发送并处理响应
+            CompletableFuture<SendBatchSmsResponse> response = client.sendBatchSms(request);
+            SendBatchSmsResponse resp = response.get();
+
+            //System.out.println("批量短信发送响应: " + new Gson().toJson(resp.getBody()));
+            log.info("批量短信发送响应: {}",new Gson().toJson(resp.getBody()));
+            return phoneNumbers.stream()
+                    .collect(Collectors.toMap(
+                            phone -> phone,
+                            phone -> "OK".equals(resp.getBody().getCode())
+                    ));
+        } catch (Exception e) {
+            //System.err.println("批量发送短信失败: " + e.getMessage());
+            log.error("批量短信发送响应: {}",e.getMessage());
+            e.printStackTrace();
+            return phoneNumbers.stream()
+                    .collect(Collectors.toMap(
+                            phone -> phone,
+                            phone -> false
+                    ));
+        }
+    }
+    /**
+     * 检查设备数据是否触发报警并发送短信
+     * 优化点:根据node_id确定唯一warningCode,直接查询单个阈值
+     */
+    public boolean checkDeviceAlarmAndSend(RealTimeData realTimeData, NodeData nodeData, List<String> alarmPhones) {
+        // 入参校验
+        if (realTimeData == null || nodeData == null) {
+            log.error("预警参数异常:设备数据或节点数据为空");
+            return false;
+        }
+        if (realTimeData.getDeviceId() <= 0) {
+            log.error("预警参数异常:设备ID无效({})", realTimeData.getDeviceId());
+            return false;
+        }
+
+        int nodeId = nodeData.getNodeId();
+        if (nodeId < 1 || nodeId > 5) {
+            log.error("节点ID无效({}),仅支持1-5的节点", nodeId);
+            return false;
+        }
+
+        try {
+            // 确定报警接收人
+            List<String> targetAlarmPhones = alarmPhones != null && !alarmPhones.isEmpty()
+                    ? alarmPhones
+                    : DEFAULT_ALARM_PHONES;
+
+            //TODO 冷处理待定
+//            String cacheKey = realTimeData.getDeviceId() + "_" + nodeId;
+//            Long lastAlarmTime = ALARM_CACHE.get(cacheKey);
+//            if (lastAlarmTime != null && System.currentTimeMillis() - lastAlarmTime < ALARM_COOLDOWN_MS) {
+//                log.info("设备{}节点{}在冷却时间内,暂不重复报警", realTimeData.getDeviceId(), nodeId);
+//                return false;
+//            }
+
+            // 根据node_id匹配唯一预警类型
+            WarningCodeEnum targetWarningType = getWarningTypeByNodeId(nodeId);
+            if (targetWarningType == null) {
+                log.warn("设备{}节点{}无对应预警类型,跳过检查", realTimeData.getDeviceId(), nodeId);
+                return false;
+            }
+            String warningCode = targetWarningType.getCode();
+            String warningMsg = targetWarningType.getName();
+
+            // 直接查询该预警类型的阈值(无需查所有再筛选)
+            double actualThreshold;
+            try {
+                String deviceCode = DeviceCodeEnum.ENVIRONMENT_DEVICE.getCode();
+                // 调用新的Service方法:根据设备编码+单个预警编码查询
+                WarningThreshold threshold = service.getWarningThresholdByDeviceAndCode(deviceCode, warningCode);
+
+                // 优先使用数据库配置,无配置则用枚举默认值
+                actualThreshold = (threshold != null && threshold.getWarningValue() != null)
+                        ? threshold.getWarningValue()
+                        : targetWarningType.getDefaultVal();
+            } catch (Exception e) {
+                log.error("查询预警阈值失败,使用默认值", e);
+                actualThreshold = targetWarningType.getDefaultVal();
+            }
+
+            // 获取当前节点的指标数值
+            double currentValue = getCurrentValueByNodeId(nodeData, nodeId);
+
+            // 判断是否超限(统一逻辑)
+            boolean isOverThreshold = currentValue > actualThreshold;
+
+            // 调试日志
+            log.debug("设备{}节点{} - 预警类型:{},当前值:{},阈值:{},是否超限:{}",
+                    realTimeData.getDeviceId(), nodeId, warningMsg, currentValue, actualThreshold, isOverThreshold);
+
+            // 触发报警
+            if (isOverThreshold) {
+//                Map<String, Object> alarmInfo = new HashMap<>();
+//                alarmInfo.put("alarmType", warningMsg);
+//                alarmInfo.put("currentValue", currentValue);
+//                alarmInfo.put("threshold", actualThreshold);
+//                alarmInfo.put("nodeId", nodeId);
+
+                log.warn("设备{}节点{}触发预警:{}(当前值:{} > 阈值:{})",
+                        realTimeData.getDeviceId(), nodeId, warningMsg, currentValue, actualThreshold);
+
+                // 构造短信参数
+                JSONObject params = new JSONObject();
+//                params.put("name", "系统管理员");
+                params.put("deviceNo", realTimeData.getDeviceId());
+//                params.put("alarmTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                params.put("alarmType", warningMsg);
+                // 经纬度处理
+                double lng = nodeData.getLng();
+                double lat = nodeData.getLat();
+                params.put("location", String.format("经纬度:%.6f,%.8f", lng, lat));
+
+//                params.put("alarmDetails", new Gson().toJson(alarmInfo));
+//                params.put("contactPerson", "设备维护组");
+//                params.put("contactPhone", "13888888888");
+
+                // 发送短信
+                Map<String, Boolean> sendResults = this.sendBatchSms(targetAlarmPhones, params.toJSONString());
+                long successCount = sendResults.values().stream().filter(Boolean::booleanValue).count();
+                log.info("设备{}报警短信发送完成,成功{}条,失败{}条",
+                        realTimeData.getDeviceId(), successCount, sendResults.size() - successCount);
+
+                // 冷却处理
+                // ALARM_CACHE.put(cacheKey, System.currentTimeMillis());
+                return true;
+            }
+            return false;
+        } catch (Exception e) {
+            log.error("设备{}节点{}报警处理失败", realTimeData.getDeviceId(), nodeId, e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据nodeId获取对应的预警类型
+     */
+    private WarningCodeEnum getWarningTypeByNodeId(int nodeId) {
+        switch (nodeId) {
+            case 1: return WarningCodeEnum.SUSPENDED_SOLIDS;
+            case 2: return WarningCodeEnum.COD;
+            case 3: return WarningCodeEnum.AMMONIA_NITROGEN;
+            case 4: return WarningCodeEnum.CONDUCTIVITY;
+            case 5: return WarningCodeEnum.PH;
+            default: return null;
+        }
+    }
+
+    /**
+     * 根据nodeId获取对应的指标数值
+     */
+    private double getCurrentValueByNodeId(NodeData nodeData, int nodeId) {
+        if (nodeId == 1) {
+            // 节点1:悬浮物(floatValue字段)
+            return nodeData.getFloatValue();
+        } else {
+            // 节点2-5:使用hum字段
+            return nodeData.getHum();
+        }
+    }
+}

+ 6 - 0
zk-common/pom.xml

@@ -111,6 +111,12 @@
             <artifactId>jjwt</artifactId>
             <version>0.9.1</version>
         </dependency>
+        <!--    阿里云发送短信服务-->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibabacloud-dysmsapi20170525</artifactId>
+            <version>4.0.3</version>
+        </dependency>
     </dependencies>
 
 </project>