diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index a26d259..b075d3c 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -60,6 +60,7 @@ jobs:
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v3
if: github.event_name != 'pull_request'
+ continue-on-error: true
with:
ignore-maven-wrapper: true
@@ -97,5 +98,5 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
- ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
- ghcr.io/qaiu/netdisk-fast-download:latest
+ ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}
+ ghcr.io/${{ github.repository }}:latest
diff --git a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java
index b22039f..12e89ab 100644
--- a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java
+++ b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java
@@ -17,7 +17,7 @@ import org.slf4j.LoggerFactory;
*
* @author QAIU
*/
-public class JDBCPoolInit {
+public class JDBCPoolInit implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
@@ -101,4 +101,16 @@ public class JDBCPoolInit {
synchronized public JDBCPool getPool() {
return pool;
}
+
+ /**
+ * 关闭连接池,释放数据库资源
+ */
+ @Override
+ public synchronized void close() {
+ if (pool != null) {
+ pool.close();
+ LOGGER.info("数据库连接池已关闭: URL={}", url);
+ pool = null;
+ }
+ }
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java
index e6717be..09c2f9f 100644
--- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java
+++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java
@@ -65,7 +65,11 @@ public final class Deploy {
// 读取yml配置
ConfigUtil.readYamlConfig(path.toString(), tempVertx)
.onSuccess(this::readConf)
- .onFailure(Throwable::printStackTrace);
+ .onFailure(err -> {
+ LOGGER.error("读取配置文件失败: {}", err.getMessage(), err);
+ LockSupport.unpark(mainThread);
+ System.exit(-1);
+ });
LockSupport.park();
deployVerticle();
}
@@ -137,6 +141,17 @@ public final class Deploy {
vertxOptions.getWorkerPoolSize());
var vertx = Vertx.vertx(vertxOptions);
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();
LocalMap localMap = sharedData.getLocalMap(LOCAL);
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java
index 0b218c0..b1ae974 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java
@@ -36,16 +36,20 @@ public final class ParamUtil {
public static MultiMap paramsToMap(String paramString) {
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
- if (paramString == null) return entries;
+ if (paramString == null || paramString.isEmpty()) return entries;
String[] params = paramString.split("&");
if (params.length == 0) return entries;
for (String param : params) {
- String[] kv = param.split("=");
+ if (param == null || param.isEmpty()) {
+ continue;
+ }
+ String[] kv = param.split("=", 2);
if (kv.length == 2) {
entries.set(kv[0], kv[1]);
- } else {
+ } else if (kv.length == 1) {
entries.set(kv[0], "");
}
+ // kv.length == 0 时(空字符串),跳过
}
return entries;
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
index 89a48ba..3d30eee 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
@@ -241,7 +241,7 @@ public final class ReflectionUtil {
public static boolean isBasicTypeArray(CtClass ctClass) {
if (!ctClass.isArray()) {
return false;
- } else return (ctClass.getName().matches("^(boolen|char|byte|short|int|long|float|double|String)\\[]$"));
+ } else return (ctClass.getName().matches("^(boolean|char|byte|short|int|long|float|double|String)\\[]$"));
}
/**
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
index 64fd990..aa713ea 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
@@ -129,16 +129,25 @@ public class HttpProxyVerticle extends AbstractVerticle {
clientRequest.response().setStatusCode(403).end();
return;
}
- String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
- if (split.length > 1) {
- // TODO
- String username = proxyServerConf.getString("username");
- String password = proxyServerConf.getString("password");
- if (!split[0].equals(username) || !split[1].equals(password)) {
- LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password);
- clientRequest.response().setStatusCode(403).end();
- return;
- }
+ String[] split;
+ try {
+ split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Proxy-Authorization header is not valid Base64");
+ clientRequest.response().setStatusCode(403).end();
+ return;
+ }
+ if (split.length <= 1) {
+ 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;
}
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java
index 8cfa6d4..717bc19 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java
@@ -46,7 +46,7 @@ public class PostExecVerticle extends AbstractVerticle {
return;
}
LOGGER.info("PostExecVerticle 开始执行...");
-
+
if (appRunImplementations != null && !appRunImplementations.isEmpty()) {
appRunImplementations.forEach(appRun -> {
try {
@@ -61,7 +61,7 @@ public class PostExecVerticle extends AbstractVerticle {
} else {
LOGGER.info("未找到 AppRun 接口的实现类");
}
-
+
LOGGER.info("PostExecVerticle 执行完成");
startPromise.complete();
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
index 977c4d7..7715758 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
@@ -351,7 +351,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
String host = url.getHost();
int port = url.getPort();
if (port == -1) {
- port = 80;
+ port = 443;
}
String originPath = url.getPath();
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
index 946b339..0ff23d7 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
@@ -5,11 +5,15 @@ import cn.qaiu.vx.core.base.BaseAsyncService;
import cn.qaiu.vx.core.util.ReflectionUtil;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.json.JsonObject;
import io.vertx.serviceproxy.ServiceBinder;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -24,6 +28,7 @@ public class ServiceVerticle extends AbstractVerticle {
Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class);
private static final AtomicInteger ID = new AtomicInteger(1);
private static final Set> handlers;
+ private final List> consumers = new ArrayList<>();
static {
Reflections reflections = ReflectionUtil.getReflections();
@@ -39,7 +44,10 @@ public class ServiceVerticle extends AbstractVerticle {
try {
serviceNames.append(asyncService.getName()).append("|");
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
- binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance);
+ String address = asInstance.getAddress();
+ MessageConsumer consumer = binder.setAddress(address)
+ .register(asInstance.getAsyncInterfaceClass(), asInstance);
+ consumers.add(consumer);
} catch (Exception e) {
LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
}
@@ -49,4 +57,19 @@ public class ServiceVerticle extends AbstractVerticle {
}
startPromise.complete();
}
+
+ @Override
+ public void stop(Promise 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();
+ }
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java
index c8c4d5d..b6a5e70 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java
@@ -32,7 +32,7 @@ public class HttpProxyConf {
public HttpProxyConf() {
this.username = DEFAULT_USERNAME;
this.password = DEFAULT_PASSWORD;
- this.timeout = DEFAULT_PORT;
+ this.port = DEFAULT_PORT;
this.timeout = DEFAULT_TIMEOUT;
this.preProxyOptions = new ProxyOptions();
}
diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java
index c1bc071..7ea50c4 100644
--- a/parser/src/main/java/cn/qaiu/parser/PanBase.java
+++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java
@@ -40,28 +40,34 @@ public abstract class PanBase implements IPanTool {
protected Promise promise = Promise.promise();
/**
- * Http client
+ * 共享的 WebClient 实例(线程安全,避免每请求创建导致资源泄漏)
*/
- protected WebClient client = WebClient.create(WebClientVertxInit.get(),
+ private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get(),
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 session (会话管理, 带cookie请求)
+ * Http client (默认使用共享实例,代理模式下使用独立实例)
+ */
+ protected WebClient client = SHARED_CLIENT;
+
+ /**
+ * Http client session (会话管理, 带cookie请求, 每实例独立)
*/
protected WebClientSession clientSession = WebClientSession.create(client);
/**
* Http client 不自动跳转
*/
- protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
- new WebClientOptions().setFollowRedirects(false));
+ protected WebClient clientNoRedirects = SHARED_CLIENT_NO_REDIRECTS;
/**
* Http client disable UserAgent
*/
- protected WebClient clientDisableUA = WebClient.create(WebClientVertxInit.get()
- , new WebClientOptions().setUserAgentEnabled(false)
- );
+ protected WebClient clientDisableUA = SHARED_CLIENT_DISABLE_UA;
protected ShareLinkInfo shareLinkInfo;
diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
index 24dbeef..0442903 100644
--- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
@@ -61,7 +61,7 @@ public class 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.headers = MultiMap.caseInsensitiveMultiMap();
// 设置默认的Accept-Encoding头以支持压缩响应
@@ -677,4 +677,13 @@ public class JsHttpClient {
return buffer.length();
}
}
+
+ /**
+ * 关闭 WebClient 释放连接池资源
+ */
+ public void close() {
+ if (client != null) {
+ client.close();
+ }
+ }
}
diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
index 6b81116..69310b4 100644
--- a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
@@ -29,12 +29,13 @@ import java.util.stream.Collectors;
* @author QAIU
* Create at 2025/10/17
*/
-public class JsParserExecutor implements IPanTool {
-
+public class JsParserExecutor implements IPanTool, AutoCloseable {
+
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
-
- private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
-
+
+ private static WorkerExecutor EXECUTOR;
+ private static final Object EXECUTOR_LOCK = new Object();
+
private static String FETCH_RUNTIME_JS = null;
private final CustomParserConfig config;
@@ -146,12 +147,57 @@ public class JsParserExecutor implements IPanTool {
}
}
+ /**
+ * 释放资源(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
public Future parse() {
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
- return EXECUTOR.executeBlocking(() -> {
+ return getExecutor().executeBlocking(() -> {
// 直接调用全局parse函数
Object parseFunction = engine.get("parse");
if (parseFunction == null) {
@@ -173,7 +219,7 @@ public class JsParserExecutor implements IPanTool {
} else {
throw new RuntimeException("parse函数类型错误");
}
- });
+ }).onComplete(ar -> close());
}
@Override
@@ -181,7 +227,7 @@ public class JsParserExecutor implements IPanTool {
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
- return EXECUTOR.executeBlocking(() -> {
+ return getExecutor().executeBlocking(() -> {
// 直接调用全局parseFileList函数
Object parseFileListFunction = engine.get("parseFileList");
if (parseFileListFunction == null) {
@@ -206,7 +252,7 @@ public class JsParserExecutor implements IPanTool {
} else {
throw new RuntimeException("parseFileList函数类型错误");
}
- });
+ }).onComplete(ar -> close());
}
@Override
@@ -214,7 +260,7 @@ public class JsParserExecutor implements IPanTool {
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
- return EXECUTOR.executeBlocking(() -> {
+ return getExecutor().executeBlocking(() -> {
// 直接调用全局parseById函数
Object parseByIdFunction = engine.get("parseById");
if (parseByIdFunction == null) {
@@ -237,7 +283,7 @@ public class JsParserExecutor implements IPanTool {
} else {
throw new RuntimeException("parseById函数类型错误");
}
- });
+ }).onComplete(ar -> close());
}
/**
diff --git a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
index 62f8a9c..453a294 100644
--- a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
+++ b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
@@ -21,11 +21,11 @@ import static cn.qaiu.util.AESUtils.encrypt;
*/
public class JsExecUtils {
private static final Invocable inv;
+ private static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager();
// 初始化脚本引擎
static {
- ScriptEngineManager engineManager = new ScriptEngineManager();
- ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
+ ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
try {
engine.eval(JsContent.ye123);
@@ -45,12 +45,11 @@ public class JsExecUtils {
}
/**
- * 调用执行蓝奏云js文件
+ * 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎)
*/
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
NoSuchMethodException {
- ScriptEngineManager engineManager = new ScriptEngineManager();
- ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
+ ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
engine.eval(JsContent.lz + "\n" + jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
@@ -63,12 +62,11 @@ public class JsExecUtils {
/**
- * 调用执行蓝奏云js文件
+ * 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
*/
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
NoSuchMethodException {
- ScriptEngineManager engineManager = new ScriptEngineManager();
- ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
+ ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
engine.eval(jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
diff --git a/parser/src/test/resources/fetch-runtime.js b/parser/src/test/resources/fetch-runtime.js
new file mode 100644
index 0000000..6d46086
--- /dev/null
+++ b/parser/src/test/resources/fetch-runtime.js
@@ -0,0 +1,329 @@
+// ==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}
+ */
+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;
+}
diff --git a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java
index 90cf3a7..77f86f1 100644
--- a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java
+++ b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java
@@ -200,6 +200,51 @@ public class CacheManager {
return promise.future();
}
+ /**
+ * 清理过期缓存记录,防止数据库无限增长
+ * @return 删除的行数
+ */
+ public Future cleanupExpiredCache() {
+ String sql = "DELETE FROM cache_link_info WHERE expiration > 0 AND expiration < #{now}";
+ Map params = new HashMap<>();
+ params.put("now", System.currentTimeMillis());
+ Promise 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