mirror of
https://gitee.com/NexIoT/Universal-IoT-Java.git
synced 2026-10-31 09:31:28 +08:00
版本更新汇总 (2025-08-10 至 2025-09-23) 核心功能优化: - 新增UDP模块并默认开启,TCP基础上增加UDP适配 - 支持网关绑定多产品子设备,优化设备重连机制 - 新增Ezviz(萤石)接入模块,调整imoulife支持自有平台账号 性能与稳定性: - 优化设备影子热数据存储,增加强制刷盘功能 - 产品列表及MQTT查询增加多级缓存策略 - 强化TCP安全机制,新增异常攻击封禁策略 功能完善: - 集成rulego可视化规则引擎基础框架 - Web-IDE新增20+ modbus/hex封装方法 - 优化乐城播放器界面及TCP连接信息展示 问题修复: - 解决系统间强依赖及注入相关问题 - 修复时间格式化、设备创建时间显示问题 - 完善日志处理及应用推送相关异常 其他改进: - 重构TCP粘包处理,删除冗余代码 - 增加Docker部署配置 - 细化权限控制策略
270 lines
8.5 KiB
Java
270 lines
8.5 KiB
Java
/*
|
|
*
|
|
* Copyright (c) 2025, IoT-Universal. All Rights Reserved.
|
|
*
|
|
* @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
|
|
* @Author: Aleo
|
|
* @Email: wo8335224@gmail.com
|
|
* @Wechat: outlookFil
|
|
*
|
|
*
|
|
*/
|
|
|
|
package cn.universal.persistence.base;
|
|
|
|
import cn.hutool.core.date.DateUtil;
|
|
import cn.hutool.core.exceptions.ExceptionUtil;
|
|
import cn.hutool.crypto.digest.MD5;
|
|
import cn.hutool.http.Header;
|
|
import cn.hutool.http.HttpException;
|
|
import cn.hutool.http.HttpRequest;
|
|
import cn.hutool.http.HttpResponse;
|
|
import cn.hutool.http.HttpUtil;
|
|
import com.github.benmanes.caffeine.cache.Cache;
|
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.TimeUnit;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
@Slf4j
|
|
public class CommonRequest {
|
|
|
|
/** http请求超时时间 */
|
|
private static final Integer HTTP_TIME_OUT = 1200;
|
|
|
|
/** URL状态信息 */
|
|
public static class UrlStatus {
|
|
|
|
private int failureCount = 0;
|
|
private long lastFailureTime = 0;
|
|
private long nextAllowTime = 0;
|
|
|
|
public UrlStatus() {}
|
|
|
|
public int getFailureCount() {
|
|
return failureCount;
|
|
}
|
|
|
|
public void setFailureCount(int failureCount) {
|
|
this.failureCount = failureCount;
|
|
}
|
|
|
|
public long getLastFailureTime() {
|
|
return lastFailureTime;
|
|
}
|
|
|
|
public void setLastFailureTime(long lastFailureTime) {
|
|
this.lastFailureTime = lastFailureTime;
|
|
}
|
|
|
|
public long getNextAllowTime() {
|
|
return nextAllowTime;
|
|
}
|
|
|
|
public void setNextAllowTime(long nextAllowTime) {
|
|
this.nextAllowTime = nextAllowTime;
|
|
}
|
|
}
|
|
|
|
private static String CACHE_NOTICE = "DNotice:";
|
|
|
|
// 存储URL状态信息,30分钟后过期
|
|
private static Cache<String, UrlStatus> urlStatusCache =
|
|
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).maximumSize(256).build();
|
|
|
|
/** 分级阈值配置 */
|
|
private static final int LEVEL_1_THRESHOLD = 3; // 轻微故障阈值
|
|
|
|
private static final int LEVEL_2_THRESHOLD = 6; // 中等故障阈值
|
|
private static final int LEVEL_3_THRESHOLD = 10; // 严重故障阈值
|
|
|
|
/** 基础延迟时间(秒) */
|
|
private static final int BASE_DELAY_SECONDS = 30;
|
|
|
|
/** 最大延迟时间(分钟) */
|
|
private static final int MAX_DELAY_MINUTES = 10;
|
|
|
|
private static ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
|
|
|
public static void downTransfer(String url, String body) {
|
|
executor.execute(() -> doRequest(url, body));
|
|
}
|
|
|
|
public static String downTransferSync(String url, String body) {
|
|
return doRequest(url, body);
|
|
}
|
|
|
|
public static String doRequest(String url, String body) {
|
|
return doRequest(url, body, false);
|
|
}
|
|
|
|
public static String doRequest(String url, String body, boolean needMd5) {
|
|
// 检查URL是否被限制访问
|
|
if (isUrlBlocked(url)) {
|
|
UrlStatus status = urlStatusCache.getIfPresent(url);
|
|
long remainingSeconds = (status.getNextAllowTime() - System.currentTimeMillis()) / 1000;
|
|
log.warn("当前地址[{}]因连续失败{}次,暂时限制访问,还需等待{}秒", url, status.getFailureCount(), remainingSeconds);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
HttpRequest request = HttpUtil.createPost(url);
|
|
request.timeout(HTTP_TIME_OUT);
|
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
|
String signature = MD5.create().digestHex16((timestamp + body));
|
|
request.header("X-Timestamp", timestamp);
|
|
request.header("X-Signature", signature);
|
|
request.header(Header.CONTENT_TYPE, "application/json");
|
|
request.body(body);
|
|
HttpResponse postBody = request.execute();
|
|
String result = postBody.body();
|
|
if (postBody == null || postBody.getStatus() != 200) {
|
|
log.warn("推送第三方失败,url={} body={} 返回={}", url, body, result);
|
|
handleFailure(url);
|
|
return result;
|
|
}
|
|
log.info("推送第三方成功={},内容={}", url, body);
|
|
handleSuccess(url);
|
|
return result;
|
|
} catch (HttpException e) {
|
|
log.warn("推送第三方失败,url={} body={} 异常={}", url, body, ExceptionUtil.getRootCause(e));
|
|
handleFailure(url);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 检查URL是否被阻止 */
|
|
private static boolean isUrlBlocked(String url) {
|
|
UrlStatus status = urlStatusCache.getIfPresent(url);
|
|
if (status == null) {
|
|
return false;
|
|
}
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
return currentTime < status.getNextAllowTime();
|
|
}
|
|
|
|
/** 处理请求成功 */
|
|
private static void handleSuccess(String url) {
|
|
UrlStatus status = urlStatusCache.getIfPresent(url);
|
|
if (status != null) {
|
|
// 成功后重置失败计数,但保留状态以避免频繁创建对象
|
|
status.setFailureCount(0);
|
|
status.setNextAllowTime(0);
|
|
log.debug("URL[{}]请求成功,重置失败计数", url);
|
|
}
|
|
}
|
|
|
|
/** 处理请求失败 - 智能退避策略 */
|
|
private static void handleFailure(String url) {
|
|
UrlStatus status = urlStatusCache.getIfPresent(url);
|
|
if (status == null) {
|
|
status = new UrlStatus();
|
|
urlStatusCache.put(url, status);
|
|
}
|
|
|
|
status.setFailureCount(status.getFailureCount() + 1);
|
|
status.setLastFailureTime(System.currentTimeMillis());
|
|
|
|
int failureCount = status.getFailureCount();
|
|
long delayMillis = calculateDelay(failureCount);
|
|
status.setNextAllowTime(System.currentTimeMillis() + delayMillis);
|
|
|
|
String delayInfo = formatDelayInfo(delayMillis);
|
|
log.warn(
|
|
"URL[{}]第{}次失败,采用{}策略,下次可访问时间:{}",
|
|
url,
|
|
failureCount,
|
|
getStrategyLevel(failureCount),
|
|
delayInfo);
|
|
|
|
// 严重故障时发送通知
|
|
if (failureCount >= LEVEL_3_THRESHOLD) {
|
|
sendFailureNotification(url, failureCount);
|
|
}
|
|
}
|
|
|
|
/** 计算延迟时间 - 指数退避算法 */
|
|
private static long calculateDelay(int failureCount) {
|
|
if (failureCount <= 1) {
|
|
return 0; // 首次失败不延迟
|
|
}
|
|
|
|
long delaySeconds;
|
|
|
|
if (failureCount <= LEVEL_1_THRESHOLD) {
|
|
// 轻微故障:线性增长 30s, 60s, 90s
|
|
delaySeconds = failureCount * BASE_DELAY_SECONDS;
|
|
} else if (failureCount <= LEVEL_2_THRESHOLD) {
|
|
// 中等故障:指数增长,但有上限
|
|
delaySeconds = (long) (BASE_DELAY_SECONDS * Math.pow(2, failureCount - LEVEL_1_THRESHOLD));
|
|
} else {
|
|
// 严重故障:固定长时间延迟
|
|
delaySeconds = MAX_DELAY_MINUTES * 60;
|
|
}
|
|
|
|
// 限制最大延迟时间
|
|
delaySeconds = Math.min(delaySeconds, MAX_DELAY_MINUTES * 60);
|
|
|
|
return delaySeconds * 1000; // 转换为毫秒
|
|
}
|
|
|
|
/** 获取策略级别描述 */
|
|
private static String getStrategyLevel(int failureCount) {
|
|
if (failureCount <= LEVEL_1_THRESHOLD) {
|
|
return "轻微故障-线性退避";
|
|
} else if (failureCount <= LEVEL_2_THRESHOLD) {
|
|
return "中等故障-指数退避";
|
|
} else {
|
|
return "严重故障-长时间限制";
|
|
}
|
|
}
|
|
|
|
/** 格式化延迟信息 */
|
|
private static String formatDelayInfo(long delayMillis) {
|
|
long seconds = delayMillis / 1000;
|
|
if (seconds < 60) {
|
|
return seconds + "秒后";
|
|
} else {
|
|
long minutes = seconds / 60;
|
|
long remainingSeconds = seconds % 60;
|
|
if (remainingSeconds == 0) {
|
|
return minutes + "分钟后";
|
|
} else {
|
|
return minutes + "分" + remainingSeconds + "秒后";
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 发送失败通知 */
|
|
private static void sendFailureNotification(String url, int failureCount) {
|
|
String noticeKey = CACHE_NOTICE + url;
|
|
UrlStatus noticeStatus = urlStatusCache.getIfPresent(noticeKey);
|
|
|
|
// 每小时最多发送一次通知
|
|
if (noticeStatus == null
|
|
|| (System.currentTimeMillis() - noticeStatus.getLastFailureTime())
|
|
> TimeUnit.HOURS.toMillis(1)) {
|
|
|
|
log.error("严重故障警告:URL[{}]连续失败{}次,已进入长时间限制状态", url, failureCount);
|
|
|
|
// 记录通知时间
|
|
UrlStatus newNoticeStatus = new UrlStatus();
|
|
newNoticeStatus.setLastFailureTime(System.currentTimeMillis());
|
|
urlStatusCache.put(noticeKey, newNoticeStatus);
|
|
|
|
// TODO: 集成钉钉/企微等通知
|
|
// DingTalkUtil.send("严重故障:地址 " + url + " 连续失败 " + failureCount + " 次");
|
|
}
|
|
}
|
|
|
|
private static void md5(String body, boolean needMd5, HttpRequest request) {
|
|
if (needMd5) {
|
|
String now = DateUtil.now();
|
|
request.header("times", now);
|
|
request.header("cn-universal-sign", MD5.create().digestHex(body + now));
|
|
}
|
|
}
|
|
}
|