mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-07-15 16:01:15 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b05b2f15b9 | |||
| 895661b8df | |||
| 93d676207c | |||
| b3083c1286 |
@@ -60,7 +60,6 @@ jobs:
|
|||||||
- name: Update dependency graph
|
- name: Update dependency graph
|
||||||
uses: advanced-security/maven-dependency-submission-action@v3
|
uses: advanced-security/maven-dependency-submission-action@v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
continue-on-error: true
|
|
||||||
with:
|
with:
|
||||||
ignore-maven-wrapper: true
|
ignore-maven-wrapper: true
|
||||||
|
|
||||||
@@ -98,5 +97,5 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}
|
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
|
||||||
ghcr.io/${{ github.repository }}:latest
|
ghcr.io/qaiu/netdisk-fast-download:latest
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
|||||||
|
|
||||||
**注意⚠️小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
**注意⚠️小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||||
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
|
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
|
||||||
|
**注意⚠️123云盘解析可能受账号地区、访问区域、出口IP等因素影响,即使已配置登录认证,跨区域访问仍可能失败,通常更接近网盘侧限制/风控,和本程序无关**
|
||||||
**注意⚠️请不要过度依赖 lz.qaiu.top,建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制,遇到解析失败的分享链接不要着急提issues,请先检查分享是否有效。**
|
**注意⚠️请不要过度依赖 lz.qaiu.top,建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制,遇到解析失败的分享链接不要着急提issues,请先检查分享是否有效。**
|
||||||
|
|
||||||
## 网盘支持情况:
|
## 网盘支持情况:
|
||||||
@@ -334,7 +335,7 @@ json返回数据格式示例:
|
|||||||
| 移动云云空间(个人版) | √ | √(密码可忽略) | 5G(个人) | 不限大小 |
|
| 移动云云空间(个人版) | √ | √(密码可忽略) | 5G(个人) | 不限大小 |
|
||||||
| 小飞机网盘 | √ | √ | 10G | 不限大小 |
|
| 小飞机网盘 | √ | √ | 10G | 不限大小 |
|
||||||
| 360亿方云 | √ | √ | 100G(须实名) | 不限大小 |
|
| 360亿方云 | √ | √ | 100G(须实名) | 不限大小 |
|
||||||
| 123云盘 | √ | √ | 2T | 100G(>100M需要登录) |
|
| 123云盘 | x | √ | 2T | 100G(>100M需要登录) |
|
||||||
| 文叔叔 | √ | √ | 10G | 5GB |
|
| 文叔叔 | √ | √ | 10G | 5GB |
|
||||||
| WPS云文档 | √ | X | 5G(免费) | 10M(免费)/2G(会员) |
|
| WPS云文档 | √ | X | 5G(免费) | 10M(免费)/2G(会员) |
|
||||||
| 夸克网盘 | x | √ | 10G | 不限大小 |
|
| 夸克网盘 | x | √ | 10G | 不限大小 |
|
||||||
@@ -492,6 +493,8 @@ auths:
|
|||||||
|
|
||||||
**注意:** 目前仅支持 123(ye)的认证配置。
|
**注意:** 目前仅支持 123(ye)的认证配置。
|
||||||
|
|
||||||
|
**补充说明:** 123(ye)登录认证是必要条件之一,但不保证所有部署环境都稳定可用;若公网云服务器解析异常而本地/NAS环境正常,通常更可能与123侧跨区域访问限制或风控相关。建议优先在本地、家庭宽带或NAS环境部署,必要时再结合代理。
|
||||||
|
|
||||||
|
|
||||||
**技术栈:**
|
**技术栈:**
|
||||||
Jdk17+Vert.x4
|
Jdk17+Vert.x4
|
||||||
@@ -516,6 +519,3 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
*/
|
*/
|
||||||
public class JDBCPoolInit implements AutoCloseable {
|
public class JDBCPoolInit {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||||
|
|
||||||
@@ -101,16 +101,4 @@ public class JDBCPoolInit implements AutoCloseable {
|
|||||||
synchronized public JDBCPool getPool() {
|
synchronized public JDBCPool getPool() {
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭连接池,释放数据库资源
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void close() {
|
|
||||||
if (pool != null) {
|
|
||||||
pool.close();
|
|
||||||
LOGGER.info("数据库连接池已关闭: URL={}", url);
|
|
||||||
pool = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,7 @@ public final class Deploy {
|
|||||||
// 读取yml配置
|
// 读取yml配置
|
||||||
ConfigUtil.readYamlConfig(path.toString(), tempVertx)
|
ConfigUtil.readYamlConfig(path.toString(), tempVertx)
|
||||||
.onSuccess(this::readConf)
|
.onSuccess(this::readConf)
|
||||||
.onFailure(err -> {
|
.onFailure(Throwable::printStackTrace);
|
||||||
LOGGER.error("读取配置文件失败: {}", err.getMessage(), err);
|
|
||||||
LockSupport.unpark(mainThread);
|
|
||||||
System.exit(-1);
|
|
||||||
});
|
|
||||||
LockSupport.park();
|
LockSupport.park();
|
||||||
deployVerticle();
|
deployVerticle();
|
||||||
}
|
}
|
||||||
@@ -141,17 +137,6 @@ public final class Deploy {
|
|||||||
vertxOptions.getWorkerPoolSize());
|
vertxOptions.getWorkerPoolSize());
|
||||||
var vertx = Vertx.vertx(vertxOptions);
|
var vertx = Vertx.vertx(vertxOptions);
|
||||||
VertxHolder.init(vertx);
|
VertxHolder.init(vertx);
|
||||||
|
|
||||||
// 注册 ShutdownHook,确保进程退出时优雅关闭资源
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
LOGGER.info("JVM shutting down, closing Vert.x...");
|
|
||||||
try {
|
|
||||||
vertx.close().toCompletionStage().toCompletableFuture().get(10, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
LOGGER.info("Vert.x closed successfully");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("Vert.x close error or timeout", e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
//配置保存在共享数据中
|
//配置保存在共享数据中
|
||||||
var sharedData = vertx.sharedData();
|
var sharedData = vertx.sharedData();
|
||||||
LocalMap<String, Object> localMap = sharedData.getLocalMap(LOCAL);
|
LocalMap<String, Object> localMap = sharedData.getLocalMap(LOCAL);
|
||||||
|
|||||||
@@ -36,20 +36,16 @@ public final class ParamUtil {
|
|||||||
|
|
||||||
public static MultiMap paramsToMap(String paramString) {
|
public static MultiMap paramsToMap(String paramString) {
|
||||||
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
|
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
|
||||||
if (paramString == null || paramString.isEmpty()) return entries;
|
if (paramString == null) return entries;
|
||||||
String[] params = paramString.split("&");
|
String[] params = paramString.split("&");
|
||||||
if (params.length == 0) return entries;
|
if (params.length == 0) return entries;
|
||||||
for (String param : params) {
|
for (String param : params) {
|
||||||
if (param == null || param.isEmpty()) {
|
String[] kv = param.split("=");
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String[] kv = param.split("=", 2);
|
|
||||||
if (kv.length == 2) {
|
if (kv.length == 2) {
|
||||||
entries.set(kv[0], kv[1]);
|
entries.set(kv[0], kv[1]);
|
||||||
} else if (kv.length == 1) {
|
} else {
|
||||||
entries.set(kv[0], "");
|
entries.set(kv[0], "");
|
||||||
}
|
}
|
||||||
// kv.length == 0 时(空字符串),跳过
|
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ public final class ReflectionUtil {
|
|||||||
public static boolean isBasicTypeArray(CtClass ctClass) {
|
public static boolean isBasicTypeArray(CtClass ctClass) {
|
||||||
if (!ctClass.isArray()) {
|
if (!ctClass.isArray()) {
|
||||||
return false;
|
return false;
|
||||||
} else return (ctClass.getName().matches("^(boolean|char|byte|short|int|long|float|double|String)\\[]$"));
|
} else return (ctClass.getName().matches("^(boolen|char|byte|short|int|long|float|double|String)\\[]$"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -129,25 +129,16 @@ public class HttpProxyVerticle extends AbstractVerticle {
|
|||||||
clientRequest.response().setStatusCode(403).end();
|
clientRequest.response().setStatusCode(403).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String[] split;
|
String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
|
||||||
try {
|
if (split.length > 1) {
|
||||||
split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
|
// TODO
|
||||||
} catch (IllegalArgumentException e) {
|
String username = proxyServerConf.getString("username");
|
||||||
LOGGER.warn("Proxy-Authorization header is not valid Base64");
|
String password = proxyServerConf.getString("password");
|
||||||
clientRequest.response().setStatusCode(403).end();
|
if (!split[0].equals(username) || !split[1].equals(password)) {
|
||||||
return;
|
LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password);
|
||||||
}
|
clientRequest.response().setStatusCode(403).end();
|
||||||
if (split.length <= 1) {
|
return;
|
||||||
LOGGER.warn("Proxy-Authorization header format invalid: missing username:password separator");
|
}
|
||||||
clientRequest.response().setStatusCode(403).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String username = proxyServerConf.getString("username");
|
|
||||||
String password = proxyServerConf.getString("password");
|
|
||||||
if (!split[0].equals(username) || !split[1].equals(password)) {
|
|
||||||
LOGGER.info("-----auth failed------\nusername: {}", split[0]);
|
|
||||||
clientRequest.response().setStatusCode(403).end();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
|||||||
String host = url.getHost();
|
String host = url.getHost();
|
||||||
int port = url.getPort();
|
int port = url.getPort();
|
||||||
if (port == -1) {
|
if (port == -1) {
|
||||||
port = 443;
|
port = 80;
|
||||||
}
|
}
|
||||||
String originPath = url.getPath();
|
String originPath = url.getPath();
|
||||||
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
|
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import cn.qaiu.vx.core.base.BaseAsyncService;
|
|||||||
import cn.qaiu.vx.core.util.ReflectionUtil;
|
import cn.qaiu.vx.core.util.ReflectionUtil;
|
||||||
import io.vertx.core.AbstractVerticle;
|
import io.vertx.core.AbstractVerticle;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.eventbus.MessageConsumer;
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import io.vertx.serviceproxy.ServiceBinder;
|
import io.vertx.serviceproxy.ServiceBinder;
|
||||||
import org.reflections.Reflections;
|
import org.reflections.Reflections;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -28,7 +24,6 @@ public class ServiceVerticle extends AbstractVerticle {
|
|||||||
Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class);
|
Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class);
|
||||||
private static final AtomicInteger ID = new AtomicInteger(1);
|
private static final AtomicInteger ID = new AtomicInteger(1);
|
||||||
private static final Set<Class<?>> handlers;
|
private static final Set<Class<?>> handlers;
|
||||||
private final List<MessageConsumer<JsonObject>> consumers = new ArrayList<>();
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Reflections reflections = ReflectionUtil.getReflections();
|
Reflections reflections = ReflectionUtil.getReflections();
|
||||||
@@ -44,10 +39,7 @@ public class ServiceVerticle extends AbstractVerticle {
|
|||||||
try {
|
try {
|
||||||
serviceNames.append(asyncService.getName()).append("|");
|
serviceNames.append(asyncService.getName()).append("|");
|
||||||
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
|
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
|
||||||
String address = asInstance.getAddress();
|
binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance);
|
||||||
MessageConsumer<JsonObject> consumer = binder.setAddress(address)
|
|
||||||
.register(asInstance.getAsyncInterfaceClass(), asInstance);
|
|
||||||
consumers.add(consumer);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
|
LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
|
||||||
}
|
}
|
||||||
@@ -57,19 +49,4 @@ public class ServiceVerticle extends AbstractVerticle {
|
|||||||
}
|
}
|
||||||
startPromise.complete();
|
startPromise.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop(Promise<Void> stopPromise) {
|
|
||||||
int count = consumers.size();
|
|
||||||
consumers.forEach(consumer -> {
|
|
||||||
try {
|
|
||||||
consumer.unregister();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("Failed to unregister service consumer at address: {}", consumer.address(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
consumers.clear();
|
|
||||||
LOGGER.info("ServiceVerticle stopped, unregistered {} services", count);
|
|
||||||
stopPromise.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class HttpProxyConf {
|
|||||||
public HttpProxyConf() {
|
public HttpProxyConf() {
|
||||||
this.username = DEFAULT_USERNAME;
|
this.username = DEFAULT_USERNAME;
|
||||||
this.password = DEFAULT_PASSWORD;
|
this.password = DEFAULT_PASSWORD;
|
||||||
this.port = DEFAULT_PORT;
|
this.timeout = DEFAULT_PORT;
|
||||||
this.timeout = DEFAULT_TIMEOUT;
|
this.timeout = DEFAULT_TIMEOUT;
|
||||||
this.preProxyOptions = new ProxyOptions();
|
this.preProxyOptions = new ProxyOptions();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,34 +40,28 @@ public abstract class PanBase implements IPanTool {
|
|||||||
protected Promise<String> promise = Promise.promise();
|
protected Promise<String> promise = Promise.promise();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 共享的 WebClient 实例(线程安全,避免每请求创建导致资源泄漏)
|
* Http client
|
||||||
*/
|
*/
|
||||||
private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get(),
|
protected WebClient client = WebClient.create(WebClientVertxInit.get(),
|
||||||
new WebClientOptions());
|
new WebClientOptions());
|
||||||
private static final WebClient SHARED_CLIENT_NO_REDIRECTS = WebClient.create(WebClientVertxInit.get(),
|
|
||||||
new WebClientOptions().setFollowRedirects(false));
|
|
||||||
private static final WebClient SHARED_CLIENT_DISABLE_UA = WebClient.create(WebClientVertxInit.get(),
|
|
||||||
new WebClientOptions().setUserAgentEnabled(false));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Http client (默认使用共享实例,代理模式下使用独立实例)
|
* Http client session (会话管理, 带cookie请求)
|
||||||
*/
|
|
||||||
protected WebClient client = SHARED_CLIENT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Http client session (会话管理, 带cookie请求, 每实例独立)
|
|
||||||
*/
|
*/
|
||||||
protected WebClientSession clientSession = WebClientSession.create(client);
|
protected WebClientSession clientSession = WebClientSession.create(client);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Http client 不自动跳转
|
* Http client 不自动跳转
|
||||||
*/
|
*/
|
||||||
protected WebClient clientNoRedirects = SHARED_CLIENT_NO_REDIRECTS;
|
protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
|
||||||
|
new WebClientOptions().setFollowRedirects(false));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Http client disable UserAgent
|
* Http client disable UserAgent
|
||||||
*/
|
*/
|
||||||
protected WebClient clientDisableUA = SHARED_CLIENT_DISABLE_UA;
|
protected WebClient clientDisableUA = WebClient.create(WebClientVertxInit.get()
|
||||||
|
, new WebClientOptions().setUserAgentEnabled(false)
|
||||||
|
);
|
||||||
|
|
||||||
protected ShareLinkInfo shareLinkInfo;
|
protected ShareLinkInfo shareLinkInfo;
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class JsHttpClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public JsHttpClient() {
|
public JsHttpClient() {
|
||||||
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());
|
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
|
||||||
this.clientSession = WebClientSession.create(client);
|
this.clientSession = WebClientSession.create(client);
|
||||||
this.headers = MultiMap.caseInsensitiveMultiMap();
|
this.headers = MultiMap.caseInsensitiveMultiMap();
|
||||||
// 设置默认的Accept-Encoding头以支持压缩响应
|
// 设置默认的Accept-Encoding头以支持压缩响应
|
||||||
@@ -677,13 +677,4 @@ public class JsHttpClient {
|
|||||||
return buffer.length();
|
return buffer.length();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭 WebClient 释放连接池资源
|
|
||||||
*/
|
|
||||||
public void close() {
|
|
||||||
if (client != null) {
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,11 @@ import java.util.stream.Collectors;
|
|||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
* Create at 2025/10/17
|
* Create at 2025/10/17
|
||||||
*/
|
*/
|
||||||
public class JsParserExecutor implements IPanTool, AutoCloseable {
|
public class JsParserExecutor implements IPanTool {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
||||||
|
|
||||||
private static WorkerExecutor EXECUTOR;
|
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||||
private static final Object EXECUTOR_LOCK = new Object();
|
|
||||||
|
|
||||||
private static String FETCH_RUNTIME_JS = null;
|
private static String FETCH_RUNTIME_JS = null;
|
||||||
|
|
||||||
@@ -147,57 +146,12 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 释放资源(ScriptEngine 和 HttpClient),避免内存泄漏
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
if (httpClient != null) {
|
|
||||||
httpClient.close();
|
|
||||||
}
|
|
||||||
// 清除 ScriptEngine 持有的 Java 对象引用,帮助 GC 回收
|
|
||||||
if (engine != null) {
|
|
||||||
engine.put("http", null);
|
|
||||||
engine.put("logger", null);
|
|
||||||
engine.put("shareLinkInfo", null);
|
|
||||||
engine.put("JavaFetch", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭全局 WorkerExecutor(应在应用关闭时调用)
|
|
||||||
*/
|
|
||||||
public static void shutdownExecutor() {
|
|
||||||
synchronized (EXECUTOR_LOCK) {
|
|
||||||
if (EXECUTOR != null) {
|
|
||||||
EXECUTOR.close();
|
|
||||||
EXECUTOR = null;
|
|
||||||
log.info("JsParserExecutor WorkerExecutor 已关闭");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取或创建 WorkerExecutor(懒加载)
|
|
||||||
*/
|
|
||||||
private static WorkerExecutor getExecutor() {
|
|
||||||
if (EXECUTOR != null) {
|
|
||||||
return EXECUTOR;
|
|
||||||
}
|
|
||||||
synchronized (EXECUTOR_LOCK) {
|
|
||||||
if (EXECUTOR == null) {
|
|
||||||
EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
|
||||||
}
|
|
||||||
return EXECUTOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
||||||
|
|
||||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
return getExecutor().executeBlocking(() -> {
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
// 直接调用全局parse函数
|
// 直接调用全局parse函数
|
||||||
Object parseFunction = engine.get("parse");
|
Object parseFunction = engine.get("parse");
|
||||||
if (parseFunction == null) {
|
if (parseFunction == null) {
|
||||||
@@ -219,7 +173,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parse函数类型错误");
|
throw new RuntimeException("parse函数类型错误");
|
||||||
}
|
}
|
||||||
}).onComplete(ar -> close());
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -227,7 +181,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
||||||
|
|
||||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
return getExecutor().executeBlocking(() -> {
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
// 直接调用全局parseFileList函数
|
// 直接调用全局parseFileList函数
|
||||||
Object parseFileListFunction = engine.get("parseFileList");
|
Object parseFileListFunction = engine.get("parseFileList");
|
||||||
if (parseFileListFunction == null) {
|
if (parseFileListFunction == null) {
|
||||||
@@ -252,7 +206,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parseFileList函数类型错误");
|
throw new RuntimeException("parseFileList函数类型错误");
|
||||||
}
|
}
|
||||||
}).onComplete(ar -> close());
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -260,7 +214,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
||||||
|
|
||||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
return getExecutor().executeBlocking(() -> {
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
// 直接调用全局parseById函数
|
// 直接调用全局parseById函数
|
||||||
Object parseByIdFunction = engine.get("parseById");
|
Object parseByIdFunction = engine.get("parseById");
|
||||||
if (parseByIdFunction == null) {
|
if (parseByIdFunction == null) {
|
||||||
@@ -283,7 +237,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
|||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parseById函数类型错误");
|
throw new RuntimeException("parseById函数类型错误");
|
||||||
}
|
}
|
||||||
}).onComplete(ar -> close());
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doParser(String html, String pwd, String sUrl) {
|
private void doParser(String html, String pwd, String sUrl) {
|
||||||
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段)
|
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b0 开头的路径段)
|
||||||
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) {
|
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b0[^/]+.*")) {
|
||||||
fail("该链接为蓝奏云目录分享,请使用目录解析接口");
|
fail("该链接为蓝奏云目录分享,请使用目录解析接口");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ import static cn.qaiu.util.AESUtils.encrypt;
|
|||||||
*/
|
*/
|
||||||
public class JsExecUtils {
|
public class JsExecUtils {
|
||||||
private static final Invocable inv;
|
private static final Invocable inv;
|
||||||
private static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager();
|
|
||||||
|
|
||||||
// 初始化脚本引擎
|
// 初始化脚本引擎
|
||||||
static {
|
static {
|
||||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||||
|
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||||
|
|
||||||
try {
|
try {
|
||||||
engine.eval(JsContent.ye123);
|
engine.eval(JsContent.ye123);
|
||||||
@@ -45,11 +45,12 @@ public class JsExecUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎)
|
* 调用执行蓝奏云js文件
|
||||||
*/
|
*/
|
||||||
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
|
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
|
||||||
NoSuchMethodException {
|
NoSuchMethodException {
|
||||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||||
|
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||||
engine.eval(JsContent.lz + "\n" + jsText);
|
engine.eval(JsContent.lz + "\n" + jsText);
|
||||||
Invocable inv = (Invocable) engine;
|
Invocable inv = (Invocable) engine;
|
||||||
//调用js中的函数
|
//调用js中的函数
|
||||||
@@ -62,11 +63,12 @@ public class JsExecUtils {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
|
* 调用执行蓝奏云js文件
|
||||||
*/
|
*/
|
||||||
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
|
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
|
||||||
NoSuchMethodException {
|
NoSuchMethodException {
|
||||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||||
|
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||||
engine.eval(jsText);
|
engine.eval(jsText);
|
||||||
Invocable inv = (Invocable) engine;
|
Invocable inv = (Invocable) engine;
|
||||||
//调用js中的函数
|
//调用js中的函数
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
// ==FetchRuntime==
|
|
||||||
// @name Fetch API Polyfill for ES5
|
|
||||||
// @description Fetch API and Promise implementation for ES5 JavaScript engines
|
|
||||||
// @version 1.0.0
|
|
||||||
// @author QAIU
|
|
||||||
// ==============
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Promise implementation compatible with ES5
|
|
||||||
* Supports basic Promise functionality needed for fetch API
|
|
||||||
*/
|
|
||||||
function SimplePromise(executor) {
|
|
||||||
var state = 'pending';
|
|
||||||
var value;
|
|
||||||
var handlers = [];
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function resolve(result) {
|
|
||||||
if (state !== 'pending') return;
|
|
||||||
state = 'fulfilled';
|
|
||||||
value = result;
|
|
||||||
handlers.forEach(handle);
|
|
||||||
handlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function reject(err) {
|
|
||||||
if (state !== 'pending') return;
|
|
||||||
state = 'rejected';
|
|
||||||
value = err;
|
|
||||||
handlers.forEach(handle);
|
|
||||||
handlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle(handler) {
|
|
||||||
if (state === 'pending') {
|
|
||||||
handlers.push(handler);
|
|
||||||
} else {
|
|
||||||
setTimeout(function() {
|
|
||||||
if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') {
|
|
||||||
try {
|
|
||||||
var result = handler.onFulfilled(value);
|
|
||||||
if (result && typeof result.then === 'function') {
|
|
||||||
result.then(handler.resolve, handler.reject);
|
|
||||||
} else {
|
|
||||||
handler.resolve(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
handler.reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state === 'rejected' && typeof handler.onRejected === 'function') {
|
|
||||||
try {
|
|
||||||
var result = handler.onRejected(value);
|
|
||||||
if (result && typeof result.then === 'function') {
|
|
||||||
result.then(handler.resolve, handler.reject);
|
|
||||||
} else {
|
|
||||||
handler.resolve(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
handler.reject(e);
|
|
||||||
}
|
|
||||||
} else if (state === 'rejected' && !handler.onRejected) {
|
|
||||||
handler.reject(value);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.then = function(onFulfilled, onRejected) {
|
|
||||||
return new SimplePromise(function(resolveNext, rejectNext) {
|
|
||||||
handle({
|
|
||||||
onFulfilled: onFulfilled,
|
|
||||||
onRejected: onRejected,
|
|
||||||
resolve: resolveNext,
|
|
||||||
reject: rejectNext
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this['catch'] = function(onRejected) {
|
|
||||||
return this.then(null, onRejected);
|
|
||||||
};
|
|
||||||
|
|
||||||
this['finally'] = function(onFinally) {
|
|
||||||
return this.then(
|
|
||||||
function(value) {
|
|
||||||
return SimplePromise.resolve(onFinally()).then(function() {
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(reason) {
|
|
||||||
return SimplePromise.resolve(onFinally()).then(function() {
|
|
||||||
throw reason;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
executor(resolve, reject);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static methods
|
|
||||||
SimplePromise.resolve = function(value) {
|
|
||||||
if (value && typeof value.then === 'function') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return new SimplePromise(function(resolve) {
|
|
||||||
resolve(value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.reject = function(reason) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.all = function(promises) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
var results = [];
|
|
||||||
var remaining = promises.length;
|
|
||||||
|
|
||||||
if (remaining === 0) {
|
|
||||||
resolve(results);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResult(index, value) {
|
|
||||||
results[index] = value;
|
|
||||||
remaining--;
|
|
||||||
if (remaining === 0) {
|
|
||||||
resolve(results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < promises.length; i++) {
|
|
||||||
(function(index) {
|
|
||||||
var promise = promises[index];
|
|
||||||
if (promise && typeof promise.then === 'function') {
|
|
||||||
promise.then(
|
|
||||||
function(value) { handleResult(index, value); },
|
|
||||||
reject
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
handleResult(index, promise);
|
|
||||||
}
|
|
||||||
})(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.race = function(promises) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
if (promises.length === 0) {
|
|
||||||
// Per spec, Promise.race with empty array stays pending forever
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < promises.length; i++) {
|
|
||||||
var promise = promises[i];
|
|
||||||
if (promise && typeof promise.then === 'function') {
|
|
||||||
promise.then(resolve, reject);
|
|
||||||
} else {
|
|
||||||
resolve(promise);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make Promise global if not already defined
|
|
||||||
if (typeof Promise === 'undefined') {
|
|
||||||
var Promise = SimplePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response object that mimics the Fetch API Response
|
|
||||||
*/
|
|
||||||
function FetchResponse(jsHttpResponse) {
|
|
||||||
this._jsResponse = jsHttpResponse;
|
|
||||||
this.status = jsHttpResponse.statusCode();
|
|
||||||
this.ok = this.status >= 200 && this.status < 300;
|
|
||||||
|
|
||||||
// Map HTTP status codes to standard status text
|
|
||||||
var statusTexts = {
|
|
||||||
200: 'OK',
|
|
||||||
201: 'Created',
|
|
||||||
204: 'No Content',
|
|
||||||
301: 'Moved Permanently',
|
|
||||||
302: 'Found',
|
|
||||||
304: 'Not Modified',
|
|
||||||
400: 'Bad Request',
|
|
||||||
401: 'Unauthorized',
|
|
||||||
403: 'Forbidden',
|
|
||||||
404: 'Not Found',
|
|
||||||
405: 'Method Not Allowed',
|
|
||||||
408: 'Request Timeout',
|
|
||||||
409: 'Conflict',
|
|
||||||
410: 'Gone',
|
|
||||||
500: 'Internal Server Error',
|
|
||||||
501: 'Not Implemented',
|
|
||||||
502: 'Bad Gateway',
|
|
||||||
503: 'Service Unavailable',
|
|
||||||
504: 'Gateway Timeout'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error');
|
|
||||||
this.headers = {
|
|
||||||
get: function(name) {
|
|
||||||
return jsHttpResponse.header(name);
|
|
||||||
},
|
|
||||||
has: function(name) {
|
|
||||||
return jsHttpResponse.header(name) !== null;
|
|
||||||
},
|
|
||||||
entries: function() {
|
|
||||||
var headerMap = jsHttpResponse.headers();
|
|
||||||
var entries = [];
|
|
||||||
for (var key in headerMap) {
|
|
||||||
if (headerMap.hasOwnProperty(key)) {
|
|
||||||
entries.push([key, headerMap[key]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchResponse.prototype.text = function() {
|
|
||||||
var body = this._jsResponse.body();
|
|
||||||
return SimplePromise.resolve(body || '');
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.json = function() {
|
|
||||||
var self = this;
|
|
||||||
return this.text().then(function(text) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Invalid JSON: ' + e.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.arrayBuffer = function() {
|
|
||||||
var bytes = this._jsResponse.bodyBytes();
|
|
||||||
return SimplePromise.resolve(bytes);
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.blob = function() {
|
|
||||||
// Blob not supported in ES5, return bytes
|
|
||||||
return this.arrayBuffer();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch API implementation using JavaFetch bridge
|
|
||||||
* @param {string} url - Request URL
|
|
||||||
* @param {Object} options - Fetch options (method, headers, body, etc.)
|
|
||||||
* @returns {Promise<FetchResponse>}
|
|
||||||
*/
|
|
||||||
function fetch(url, options) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
try {
|
|
||||||
// Parse options
|
|
||||||
options = options || {};
|
|
||||||
var method = (options.method || 'GET').toUpperCase();
|
|
||||||
var headers = options.headers || {};
|
|
||||||
var body = options.body;
|
|
||||||
|
|
||||||
// Prepare request options for JavaFetch
|
|
||||||
var requestOptions = {
|
|
||||||
method: method,
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert headers to simple object
|
|
||||||
if (headers) {
|
|
||||||
if (typeof headers.forEach === 'function') {
|
|
||||||
// Headers object
|
|
||||||
headers.forEach(function(value, key) {
|
|
||||||
requestOptions.headers[key] = value;
|
|
||||||
});
|
|
||||||
} else if (typeof headers === 'object') {
|
|
||||||
// Plain object
|
|
||||||
for (var key in headers) {
|
|
||||||
if (headers.hasOwnProperty(key)) {
|
|
||||||
requestOptions.headers[key] = headers[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add body if present
|
|
||||||
if (body !== undefined && body !== null) {
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
requestOptions.body = body;
|
|
||||||
} else if (typeof body === 'object') {
|
|
||||||
// Assume JSON
|
|
||||||
requestOptions.body = JSON.stringify(body);
|
|
||||||
if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) {
|
|
||||||
requestOptions.headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call JavaFetch bridge
|
|
||||||
var jsHttpResponse = JavaFetch.fetch(url, requestOptions);
|
|
||||||
|
|
||||||
// Create Response object
|
|
||||||
var response = new FetchResponse(jsHttpResponse);
|
|
||||||
resolve(response);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for global use
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.fetch = fetch;
|
|
||||||
window.Promise = Promise;
|
|
||||||
} else if (typeof global !== 'undefined') {
|
|
||||||
global.fetch = fetch;
|
|
||||||
global.Promise = Promise;
|
|
||||||
}
|
|
||||||
@@ -200,51 +200,6 @@ public class CacheManager {
|
|||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理过期缓存记录,防止数据库无限增长
|
|
||||||
* @return 删除的行数
|
|
||||||
*/
|
|
||||||
public Future<Integer> cleanupExpiredCache() {
|
|
||||||
String sql = "DELETE FROM cache_link_info WHERE expiration > 0 AND expiration < #{now}";
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
|
||||||
params.put("now", System.currentTimeMillis());
|
|
||||||
Promise<Integer> promise = Promise.promise();
|
|
||||||
SqlTemplate.forUpdate(jdbcPool, sql)
|
|
||||||
.execute(params)
|
|
||||||
.onSuccess(res -> {
|
|
||||||
int deleted = res.rowCount();
|
|
||||||
if (deleted > 0) {
|
|
||||||
LOGGER.info("清理过期缓存记录 {} 条", deleted);
|
|
||||||
}
|
|
||||||
promise.complete(deleted);
|
|
||||||
})
|
|
||||||
.onFailure(e -> {
|
|
||||||
LOGGER.error("清理过期缓存失败", e);
|
|
||||||
promise.fail(e);
|
|
||||||
});
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册定时清理过期缓存任务(每小时执行一次)
|
|
||||||
* 应在应用启动后调用
|
|
||||||
*/
|
|
||||||
public static void registerPeriodicCleanup() {
|
|
||||||
try {
|
|
||||||
io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance();
|
|
||||||
vertx.setPeriodic(3600_000, 3600_000, id -> {
|
|
||||||
try {
|
|
||||||
new CacheManager().cleanupExpiredCache();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
LOGGER.info("缓存定时清理任务已注册(每小时执行)");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("注册缓存定时清理任务失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Future<Map<String, Integer>> getShareKeyTotal(String shareKey) {
|
public Future<Map<String, Integer>> getShareKeyTotal(String shareKey) {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total
|
SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class RateLimiter {
|
|||||||
MAX_REQUESTS, TIME_WINDOW, PATH_REG);
|
MAX_REQUESTS, TIME_WINDOW, PATH_REG);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Future<Void> checkRateLimit(HttpServerRequest request) {
|
synchronized public static Future<Void> checkRateLimit(HttpServerRequest request) {
|
||||||
Promise<Void> promise = Promise.promise();
|
Promise<Void> promise = Promise.promise();
|
||||||
if (!request.path().matches(PATH_REG)) {
|
if (!request.path().matches(PATH_REG)) {
|
||||||
// 如果请求路径不匹配正则,则不进行限流
|
// 如果请求路径不匹配正则,则不进行限流
|
||||||
@@ -38,13 +38,7 @@ public class RateLimiter {
|
|||||||
|
|
||||||
String ip = request.remoteAddress().host();
|
String ip = request.remoteAddress().host();
|
||||||
|
|
||||||
// 定期清理过期条目,防止 Map 无限增长
|
ipRequestMap.compute(ip, (key, requestInfo) -> {
|
||||||
if (ipRequestMap.size() > 1000) {
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
ipRequestMap.entrySet().removeIf(entry -> now - entry.getValue().timestamp > TIME_WINDOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestInfo info = ipRequestMap.compute(ip, (key, requestInfo) -> {
|
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) {
|
if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) {
|
||||||
// 初始化或重置计数器
|
// 初始化或重置计数器
|
||||||
@@ -56,6 +50,7 @@ public class RateLimiter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RequestInfo info = ipRequestMap.get(ip);
|
||||||
if (info.count > MAX_REQUESTS) {
|
if (info.count > MAX_REQUESTS) {
|
||||||
// 超过限制
|
// 超过限制
|
||||||
// 计算剩余时间
|
// 计算剩余时间
|
||||||
@@ -71,8 +66,8 @@ public class RateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class RequestInfo {
|
private static class RequestInfo {
|
||||||
volatile int count;
|
int count;
|
||||||
volatile long timestamp;
|
long timestamp;
|
||||||
|
|
||||||
RequestInfo(int count, long time) {
|
RequestInfo(int count, long time) {
|
||||||
this.count = count;
|
this.count = count;
|
||||||
|
|||||||
@@ -29,11 +29,6 @@ public class CacheServiceImpl implements CacheService {
|
|||||||
|
|
||||||
private final CacheManager cacheManager = new CacheManager();
|
private final CacheManager cacheManager = new CacheManager();
|
||||||
|
|
||||||
static {
|
|
||||||
// 服务类加载时注册缓存定时清理任务
|
|
||||||
CacheManager.registerPeriodicCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
|
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
|
||||||
|
|
||||||
// 认证、域名相关(检查是否已经添加过参数,避免重复调用)
|
// 认证、域名相关(检查是否已经添加过参数,避免重复调用)
|
||||||
|
|||||||
Reference in New Issue
Block a user