|
|
@@ -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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|