SmsUtil.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. package com.zksy.api.utils;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.aliyun.auth.credentials.Credential;
  4. import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
  5. import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient;
  6. import com.aliyun.sdk.service.dysmsapi20170525.models.SendBatchSmsRequest;
  7. import com.aliyun.sdk.service.dysmsapi20170525.models.SendBatchSmsResponse;
  8. import com.google.gson.Gson;
  9. import com.zksy.api.config.AliyunSmsConfig;
  10. import com.zksy.api.domain.AlarmData;
  11. import com.zksy.api.domain.WarningThreshold;
  12. import com.zksy.api.service.AlarmDataService;
  13. import com.zksy.api.service.WarningThresholdService;
  14. import darabonba.core.client.ClientOverrideConfiguration;
  15. import lombok.extern.slf4j.Slf4j;
  16. import org.springframework.beans.factory.annotation.Autowired;
  17. import org.springframework.stereotype.Component;
  18. import rk.netDevice.sdk.p2.NodeData;
  19. import rk.netDevice.sdk.p2.RealTimeData;
  20. import java.math.BigDecimal;
  21. import java.time.Duration;
  22. import java.time.LocalDateTime;
  23. import java.util.Collections;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.concurrent.CompletableFuture;
  27. import java.util.stream.Collectors;
  28. @Component
  29. @Slf4j
  30. public class SmsUtil {
  31. private final AliyunSmsConfig aliyunSmsConfig;
  32. private final AsyncClient client;
  33. private final WarningThresholdService service;
  34. private final AlarmDataService alarmDataService;
  35. // 报警缓存和冷却时间
  36. // private static final Map<String, Long> ALARM_CACHE = new ConcurrentHashMap<>();
  37. // private static final long ALARM_COOLDOWN_MS = 5 * 60 * 1000;
  38. @Autowired
  39. public SmsUtil(AliyunSmsConfig aliyunSmsConfig, WarningThresholdService service, AlarmDataService alarmDataService) {
  40. this.aliyunSmsConfig = aliyunSmsConfig;
  41. this.service = service;
  42. this.alarmDataService = alarmDataService;
  43. try {
  44. StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
  45. .accessKeyId(aliyunSmsConfig.getAccessKeyId())
  46. .accessKeySecret(aliyunSmsConfig.getAccessKeySecret())
  47. .build());
  48. this.client = AsyncClient.builder()
  49. .region(aliyunSmsConfig.getRegionId())
  50. .credentialsProvider(provider)
  51. .overrideConfiguration(
  52. ClientOverrideConfiguration.create()
  53. .setEndpointOverride("dysmsapi.aliyuncs.com")
  54. .setConnectTimeout(Duration.ofSeconds(30))
  55. )
  56. .build();
  57. } catch (Exception e) {
  58. log.error("阿里云短信客户端初始化失败", e);
  59. throw new RuntimeException("短信服务初始化异常", e); // 初始化失败应快速失败
  60. }
  61. }
  62. /**
  63. * 批量发送相同内容的短信给多个手机号
  64. * @param phoneNumbers 接收短信的手机号列表
  65. * @param templateParam 短信模板参数,JSON格式字符串
  66. * @return 发送结果,key为手机号,value为是否成功
  67. */
  68. public Map<String, Boolean> sendBatchSms(List<String> phoneNumbers, String templateParam) {
  69. if (phoneNumbers == null || phoneNumbers.isEmpty()) {
  70. log.warn("批量发送短信失败:手机号列表为空");
  71. return Collections.emptyMap();
  72. }
  73. try {
  74. List<String> signNames = phoneNumbers.stream()
  75. .map(phone -> aliyunSmsConfig.getSignName())
  76. .collect(Collectors.toList());
  77. List<String> templateParams = phoneNumbers.stream()
  78. .map(phone -> templateParam)
  79. .collect(Collectors.toList());
  80. //构建批量发送请求
  81. SendBatchSmsRequest request = SendBatchSmsRequest.builder()
  82. .phoneNumberJson(new Gson().toJson(phoneNumbers)) // 手机号列表
  83. .signNameJson(new Gson().toJson(signNames)) // 签名列表
  84. .templateCode(aliyunSmsConfig.getTemplateCode())
  85. .templateParamJson(new Gson().toJson(templateParams)) // 参数列表
  86. .build();
  87. // 发送并处理响应
  88. CompletableFuture<SendBatchSmsResponse> response = client.sendBatchSms(request);
  89. SendBatchSmsResponse resp = response.get();
  90. log.info("批量短信发送响应: {}",new Gson().toJson(resp.getBody()));
  91. return phoneNumbers.stream()
  92. .collect(Collectors.toMap(
  93. phone -> phone,
  94. phone -> "OK".equals(resp.getBody().getCode())
  95. ));
  96. } catch (Exception e) {
  97. log.error("批量短信发送响应: {}",e.getMessage());
  98. e.printStackTrace();
  99. return phoneNumbers.stream()
  100. .collect(Collectors.toMap(
  101. phone -> phone,
  102. phone -> false
  103. ));
  104. }
  105. }
  106. /**
  107. * 检查设备数据是否触发报警并发送短信
  108. */
  109. public boolean checkDeviceAlarmAndSend(RealTimeData realTimeData, NodeData nodeData, List<String> alarmPhones) {
  110. if (realTimeData == null || nodeData == null) {
  111. log.error("预警参数异常:设备数据或节点数据为空");
  112. return false;
  113. }
  114. if (realTimeData.getDeviceId() <= 0) {
  115. log.error("预警参数异常:设备ID无效({})", realTimeData.getDeviceId());
  116. return false;
  117. }
  118. int nodeId = nodeData.getNodeId();
  119. if (nodeId < 1 || nodeId > 5) {
  120. log.error("节点ID无效({}),仅支持1-5的节点", nodeId);
  121. return false;
  122. }
  123. try {
  124. //TODO 冷处理待定
  125. // String cacheKey = realTimeData.getDeviceId() + "_" + nodeId;
  126. // Long lastAlarmTime = ALARM_CACHE.get(cacheKey);
  127. // if (lastAlarmTime != null && System.currentTimeMillis() - lastAlarmTime < ALARM_COOLDOWN_MS) {
  128. // log.info("设备{}节点{}在冷却时间内,暂不重复报警", realTimeData.getDeviceId(), nodeId);
  129. // return false;
  130. // }
  131. // 根据node_id获取预警编码(自定义写死,不从枚举获取)
  132. String warningCode = getWarningCodeByNodeId(nodeId);
  133. if (warningCode == null) {
  134. log.warn("设备{}节点{}无对应预警编码,跳过检查", realTimeData.getDeviceId(), nodeId);
  135. return false;
  136. }
  137. // 设备编号从设备数据获取
  138. String deviceCode = String.valueOf(realTimeData.getDeviceId());
  139. // 查询预警阈值表
  140. WarningThreshold threshold = null;
  141. try {
  142. threshold = service.getWarningThresholdByDeviceAndCode(deviceCode, warningCode);
  143. } catch (Exception e) {
  144. log.error("查询预警阈值失败", e);
  145. }
  146. // 获取预警类型,默认使用节点对应的名称
  147. String warningType = getWarningTypeNameByNodeId(nodeId);
  148. if (threshold != null && threshold.getWarningType() != null) {
  149. warningType = threshold.getWarningType();
  150. }
  151. // 获取最小值和最大值
  152. Double minValue = threshold != null ? threshold.getMinValue() : null;
  153. Double maxValue = threshold != null ? threshold.getMaxValue() : null;
  154. String remark = threshold != null ? threshold.getRemark() : null;
  155. // 获取当前节点的指标数值
  156. double currentValue = getCurrentValueByNodeId(nodeData, nodeId);
  157. // 判断是否触发报警
  158. boolean isOverThreshold = false;
  159. if (minValue != null && currentValue <= minValue) {
  160. isOverThreshold = true;
  161. }
  162. if (maxValue != null && currentValue >= maxValue) {
  163. isOverThreshold = true;
  164. }
  165. log.debug("设备{}节点{} - 预警类型:{},当前值:{},最小值:{},最大值:{},是否超限:{}",
  166. realTimeData.getDeviceId(), nodeId, warningType, currentValue, minValue, maxValue, isOverThreshold);
  167. // 触发报警
  168. if (isOverThreshold) {
  169. log.warn("设备{}节点{}触发预警:{}(当前值:{},最小值:{},最大值:{})",
  170. realTimeData.getDeviceId(), nodeId, warningType, currentValue, minValue, maxValue);
  171. // 1. 先保存 alarm_data
  172. saveAlarmData(deviceCode, warningType, warningCode,
  173. minValue != null ? BigDecimal.valueOf(minValue) : null,
  174. maxValue != null ? BigDecimal.valueOf(maxValue) : null,
  175. BigDecimal.valueOf(currentValue), remark);
  176. // 2. 再构造并发送短信
  177. JSONObject params = new JSONObject();
  178. params.put("deviceNo", realTimeData.getDeviceId());
  179. params.put("alarmType", warningType);
  180. double lng = nodeData.getLng();
  181. double lat = nodeData.getLat();
  182. params.put("location", String.format("经纬度:%.6f,%.8f", lng, lat));
  183. Map<String, Boolean> sendResults = this.sendBatchSms(alarmPhones, params.toJSONString());
  184. long successCount = sendResults.values().stream().filter(Boolean::booleanValue).count();
  185. log.info("设备{}报警短信发送完成,成功{}条,失败{}条",
  186. realTimeData.getDeviceId(), successCount, sendResults.size() - successCount);
  187. // 冷却处理
  188. // ALARM_CACHE.put(cacheKey, System.currentTimeMillis());
  189. return true;
  190. }
  191. return false;
  192. } catch (Exception e) {
  193. log.error("设备{}节点{}报警处理失败", realTimeData.getDeviceId(), nodeId, e);
  194. return false;
  195. }
  196. }
  197. /**
  198. * 保存告警数据到 alarm_data 表
  199. */
  200. private void saveAlarmData(String deviceCode, String warningType, String warningCode,
  201. BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue, String remark) {
  202. try {
  203. AlarmData alarmData = new AlarmData();
  204. alarmData.setDeviceCode(deviceCode);
  205. alarmData.setWarningType(warningType);
  206. alarmData.setWarningCode(warningCode);
  207. alarmData.setMinValue(minValue);
  208. alarmData.setMaxValue(maxValue);
  209. alarmData.setActualValue(actualValue);
  210. alarmData.setAlarmStatus(0);
  211. alarmData.setAlarmTime(LocalDateTime.now());
  212. alarmData.setRemark(remark);
  213. alarmData.setCreateTime(LocalDateTime.now());
  214. alarmDataService.saveAlarmData(alarmData);
  215. } catch (Exception e) {
  216. log.error("保存告警数据失败", e);
  217. }
  218. }
  219. /**
  220. * 根据nodeId获取对应的预警编码(自定义写死)
  221. */
  222. private String getWarningCodeByNodeId(int nodeId) {
  223. switch (nodeId) {
  224. case 1: return "WARN_SUSPENDED_SOLIDS";
  225. case 2: return "WARN_COD";
  226. case 3: return "WARN_AMMONIA_NITROGEN";
  227. case 4: return "WARN_CONDUCTIVITY";
  228. case 5: return "WARN_PH";
  229. default: return null;
  230. }
  231. }
  232. /**
  233. * 根据nodeId获取对应的预警类型名称
  234. */
  235. private String getWarningTypeNameByNodeId(int nodeId) {
  236. switch (nodeId) {
  237. case 1: return "悬浮物";
  238. case 2: return "COD";
  239. case 3: return "氨氮";
  240. case 4: return "电导率";
  241. case 5: return "pH值";
  242. default: return "未知预警";
  243. }
  244. }
  245. /**
  246. * 根据nodeId获取对应的指标数值
  247. */
  248. private double getCurrentValueByNodeId(NodeData nodeData, int nodeId) {
  249. if (nodeId == 1) {
  250. // 节点1需要特殊处理,取悬浮物字段(floatValue字段)
  251. return nodeData.getFloatValue();
  252. } else {
  253. // 其他节点取hum字段
  254. return nodeData.getHum();
  255. }
  256. }
  257. }